import * as cp from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as ts from "typescript";
import * as url from "url";
import which from "which";

const stradaFourslashPath = path.resolve(import.meta.dirname, "../", "../", "../", "_submodules", "TypeScript", "tests", "cases", "fourslash");

let inputFileSet: Set<string> | undefined;

const failingTestsPath = path.join(import.meta.dirname, "failingTests.txt");
const manualTestsPath = path.join(import.meta.dirname, "manualTests.txt");

const outputDir = path.join(import.meta.dirname, "../", "tests", "gen");

const unparsedFiles: string[] = [];

function getFailingTests(): Set<string> {
    const failingTestsList = fs.readFileSync(failingTestsPath, "utf-8").split("\n").map(line => line.trim().substring(4)).filter(line => line.length > 0);
    return new Set(failingTestsList);
}

function getManualTests(): Set<string> {
    if (!fs.existsSync(manualTestsPath)) {
        return new Set();
    }
    const manualTestsList = fs.readFileSync(manualTestsPath, "utf-8").split("\n").map(line => line.trim()).filter(line => line.length > 0);
    return new Set(manualTestsList);
}

export function main() {
    const args = process.argv.slice(2);
    const inputFilesPath = args[0];
    if (inputFilesPath) {
        const inputFiles = fs.readFileSync(inputFilesPath, "utf-8")
            .split("\n").map(line => line.trim())
            .filter(line => line.length > 0)
            .map(line => path.basename(line));
        inputFileSet = new Set(inputFiles);
    }

    fs.rmSync(outputDir, { recursive: true, force: true });
    fs.mkdirSync(outputDir, { recursive: true });

    parseTypeScriptFiles(getFailingTests(), getManualTests(), stradaFourslashPath);
    console.log(unparsedFiles.join("\n"));
    const gofmt = which.sync("go");
    cp.execFileSync(gofmt, ["tool", "mvdan.cc/gofumpt", "-lang=go1.25", "-w", outputDir]);
}

function parseTypeScriptFiles(failingTests: Set<string>, manualTests: Set<string>, folder: string): void {
    const files = fs.readdirSync(folder);

    files.forEach(file => {
        const filePath = path.join(folder, file);
        const stat = fs.statSync(filePath);
        if (inputFileSet && !inputFileSet.has(file)) {
            return;
        }

        if (stat.isDirectory()) {
            parseTypeScriptFiles(failingTests, manualTests, filePath);
        }
        else if (file.endsWith(".ts") && !manualTests.has(file.slice(0, -3))) {
            const content = fs.readFileSync(filePath, "utf-8");
            const test = parseFileContent(file, content);
            const isServer = filePath.split(path.sep).includes("server");
            if (test) {
                const testContent = generateGoTest(failingTests, test, isServer);
                const testPath = path.join(outputDir, `${test.name}_test.go`);
                fs.writeFileSync(testPath, testContent, "utf-8");
            }
        }
    });
}

function parseFileContent(filename: string, content: string): GoTest | undefined {
    console.error(`Parsing file: ${filename}`);
    const sourceFile = ts.createSourceFile("temp.ts", content, ts.ScriptTarget.Latest, true /*setParentNodes*/);
    const statements = sourceFile.statements;
    const goTest: GoTest = {
        name: filename.replace(".ts", "").replace(".", ""),
        content: getTestInput(content),
        commands: [],
    };
    for (const statement of statements) {
        const result = parseFourslashStatement(statement);
        if (!result) {
            unparsedFiles.push(filename);
            return undefined;
        }
        else {
            goTest.commands.push(...result);
        }
    }
    if (goTest.commands.length === 0) {
        console.error(`No commands parsed in file: ${filename}`);
        unparsedFiles.push(filename);
        return undefined;
    }
    return goTest;
}

function getTestInput(content: string): string {
    const lines = content.split("\n").map(line => line.endsWith("\r") ? line.slice(0, -1) : line);
    let testInput: string[] = [];
    for (const line of lines) {
        let newLine = "";
        if (line.startsWith("////")) {
            const parts = line.substring(4).split("`");
            for (let i = 0; i < parts.length; i++) {
                if (i > 0) {
                    newLine += `\` + "\`" + \``;
                }
                newLine += parts[i];
            }
            testInput.push(newLine);
        }
        else if (line.startsWith("// @") || line.startsWith("//@")) {
            testInput.push(line);
        }
        // !!! preserve non-input comments?
    }

    // chomp leading spaces
    if (
        !testInput.some(line =>
            line.length != 0 &&
            !line.startsWith(" ") &&
            !line.startsWith("// ") &&
            !line.startsWith("//@")
        )
    ) {
        testInput = testInput.map(line => {
            if (line.startsWith(" ")) return line.substring(1);
            return line;
        });
    }
    return `\`${testInput.join("\n")}\``;
}

/**
 * Parses a Strada fourslash statement and returns the corresponding Corsa commands.
 * @returns an array of commands if the statement is a valid fourslash command, or `undefined` if the statement could not be parsed.
 */
function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
    if (ts.isVariableStatement(statement)) {
        // variable declarations (for ranges and markers), e.g. `const range = test.ranges()[0];`
        return [];
    }
    else if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
        const callExpression = statement.expression;
        if (!ts.isPropertyAccessExpression(callExpression.expression)) {
            console.error(`Expected property access expression, got ${callExpression.expression.getText()}`);
            return undefined;
        }
        const namespace = callExpression.expression.expression;
        const func = callExpression.expression.name;
        if (!ts.isIdentifier(namespace)) {
            switch (func.text) {
                case "quickInfoExists":
                    return parseQuickInfoArgs("notQuickInfoExists", callExpression.arguments);
                case "andApplyCodeAction":
                    // verify.completions({ ... }).andApplyCodeAction(...)
                    if (!(ts.isCallExpression(namespace) && namespace.expression.getText() === "verify.completions")) {
                        console.error(`Unrecognized fourslash statement: ${statement.getText()}`);
                        return undefined;
                    }
                    return parseVerifyCompletionsArgs(namespace.arguments, callExpression.arguments);
            }
            console.error(`Unrecognized fourslash statement: ${statement.getText()}`);
            return undefined;
        }
        // `verify.(...)`
        if (namespace.text === "verify") {
            switch (func.text) {
                case "completions":
                    // `verify.completions(...)`
                    return parseVerifyCompletionsArgs(callExpression.arguments);
                case "applyCodeActionFromCompletion":
                    // `verify.applyCodeActionFromCompletion(...)`
                    return parseVerifyApplyCodeActionFromCompletionArgs(callExpression.arguments);
                case "importFixAtPosition":
                    // `verify.importFixAtPosition(...)`
                    return parseImportFixAtPositionArgs(callExpression.arguments);
                case "quickInfoAt":
                case "quickInfoExists":
                case "quickInfoIs":
                case "quickInfos":
                    // `verify.quickInfo...(...)`
                    return parseQuickInfoArgs(func.text, callExpression.arguments);
                case "baselineFindAllReferences":
                    // `verify.baselineFindAllReferences(...)`
                    return parseBaselineFindAllReferencesArgs(callExpression.arguments);
                case "baselineDocumentHighlights":
                    return parseBaselineDocumentHighlightsArgs(callExpression.arguments);
                case "baselineQuickInfo":
                    return parseBaselineQuickInfo(callExpression.arguments);
                case "baselineSignatureHelp":
                    return [parseBaselineSignatureHelp(callExpression.arguments)];
                case "signatureHelp":
                    return parseSignatureHelp(callExpression.arguments);
                case "noSignatureHelp":
                    return parseNoSignatureHelp(callExpression.arguments);
                case "signatureHelpPresentForTriggerReason":
                    return parseSignatureHelpPresentForTriggerReason(callExpression.arguments);
                case "noSignatureHelpForTriggerReason":
                    return parseNoSignatureHelpForTriggerReason(callExpression.arguments);
                case "baselineSmartSelection":
                    return [parseBaselineSmartSelection(callExpression.arguments)];
                case "baselineCallHierarchy":
                    return [parseBaselineCallHierarchy(callExpression.arguments)];
                case "baselineGoToDefinition":
                case "baselineGetDefinitionAtPosition":
                case "baselineGoToType":
                case "baselineGoToImplementation":
                    // Both `baselineGoToDefinition` and `baselineGetDefinitionAtPosition` take the same
                    // arguments, but differ in that...
                    //  - `verify.baselineGoToDefinition(...)` called getDefinitionAndBoundSpan
                    //  - `verify.baselineGetDefinitionAtPosition(...)` called getDefinitionAtPosition
                    // LSP doesn't have two separate commands though.
                    return parseBaselineGoToDefinitionArgs(func.text, callExpression.arguments);
                case "baselineRename":
                case "baselineRenameAtRangesWithText":
                    // `verify.baselineRename...(...)`
                    return parseBaselineRenameArgs(func.text, callExpression.arguments);
                case "baselineInlayHints":
                    return parseBaselineInlayHints(callExpression.arguments);
                case "renameInfoSucceeded":
                case "renameInfoFailed":
                    return parseRenameInfo(func.text, callExpression.arguments);
                case "getSemanticDiagnostics":
                case "getSuggestionDiagnostics":
                case "getSyntacticDiagnostics":
                    return parseVerifyDiagnostics(func.text, callExpression.arguments);
                case "baselineSyntacticDiagnostics":
                case "baselineSyntacticAndSemanticDiagnostics":
                    return [{ kind: "verifyBaselineDiagnostics" }];
                case "navigateTo":
                    return parseVerifyNavigateTo(callExpression.arguments);
                case "outliningSpansInCurrentFile":
                case "outliningHintSpansInCurrentFile":
                    return parseOutliningSpansArgs(callExpression.arguments);
                case "navigationTree":
                    return parseVerifyNavTree(callExpression.arguments);
                case "navigationBar":
                    return []; // Deprecated.
            }
        }
        // `goTo....`
        if (namespace.text === "goTo") {
            return parseGoToArgs(callExpression.arguments, func.text);
        }
        // `edit....`
        if (namespace.text === "edit") {
            const result = parseEditStatement(func.text, callExpression.arguments);
            if (!result) {
                return undefined;
            }
            return [result];
        }
        // !!! other fourslash commands
    }
    console.error(`Unrecognized fourslash statement: ${statement.getText()}`);
    return undefined;
}

function parseEditStatement(funcName: string, args: readonly ts.Expression[]): EditCmd | undefined {
    switch (funcName) {
        case "insert":
        case "paste":
        case "insertLine": {
            let arg0;
            if (args.length !== 1 || !(arg0 = getStringLiteralLike(args[0]))) {
                console.error(`Expected a single string literal argument in edit.${funcName}, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            return {
                kind: "edit",
                goStatement: `f.${funcName.charAt(0).toUpperCase() + funcName.slice(1)}(t, ${getGoStringLiteral(arg0.text)})`,
            };
        }
        case "replaceLine": {
            let arg0, arg1;
            if (args.length !== 2 || !(arg0 = getNumericLiteral(args[0])) || !(arg1 = getStringLiteralLike(args[1]))) {
                console.error(`Expected a single string literal argument in edit.insert, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            return {
                kind: "edit",
                goStatement: `f.ReplaceLine(t, ${arg0.text}, ${getGoStringLiteral(arg1.text)})`,
            };
        }
        case "backspace": {
            const arg = args[0];
            if (args[0]) {
                let arg0;
                if (!(arg0 = getNumericLiteral(arg))) {
                    console.error(`Expected numeric literal argument in edit.backspace, got ${arg.getText()}`);
                    return undefined;
                }
                return {
                    kind: "edit",
                    goStatement: `f.Backspace(t, ${arg0.text})`,
                };
            }
            return {
                kind: "edit",
                goStatement: `f.Backspace(t, 1)`,
            };
        }
        default:
            console.error(`Unrecognized edit function: ${funcName}`);
            return undefined;
    }
}

function getGoMultiLineStringLiteral(text: string): string {
    if (!text.includes("`") && !text.includes("\\")) {
        return "`" + text + "`";
    }
    return getGoStringLiteral(text);
}

function getGoStringLiteral(text: string): string {
    return `${JSON.stringify(text)}`;
}

function parseGoToArgs(args: readonly ts.Expression[], funcName: string): GoToCmd[] | undefined {
    switch (funcName) {
        case "marker": {
            const arg = args[0];
            if (arg === undefined) {
                return [{
                    kind: "goTo",
                    funcName: "marker",
                    args: [`""`],
                }];
            }
            let strArg;
            if (!(strArg = getStringLiteralLike(arg))) {
                console.error(`Unrecognized argument in goTo.marker: ${arg.getText()}`);
                return undefined;
            }
            return [{
                kind: "goTo",
                funcName: "marker",
                args: [getGoStringLiteral(strArg.text)],
            }];
        }
        case "file": {
            if (args.length !== 1) {
                console.error(`Expected a single argument in goTo.file, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            let arg0;
            if (arg0 = getStringLiteralLike(args[0])) {
                const text = arg0.text.replace("tests/cases/fourslash/server/", "").replace("tests/cases/fourslash/", "");
                return [{
                    kind: "goTo",
                    funcName: "file",
                    args: [getGoStringLiteral(text)],
                }];
            }
            else if (arg0 = getNumericLiteral(args[0])) {
                return [{
                    kind: "goTo",
                    funcName: "fileNumber",
                    args: [arg0.text],
                }];
            }
            console.error(`Expected string or number literal argument in goTo.file, got ${args[0].getText()}`);
            return undefined;
        }
        case "position": {
            let arg0;
            if (args.length !== 1 || !(arg0 = getNumericLiteral(args[0]))) {
                console.error(`Expected a single numeric literal argument in goTo.position, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            return [{
                kind: "goTo",
                funcName: "position",
                args: [`${arg0.text}`],
            }];
        }
        case "eof":
            return [{
                kind: "goTo",
                funcName: "EOF",
                args: [],
            }];
        case "bof":
            return [{
                kind: "goTo",
                funcName: "BOF",
                args: [],
            }];
        case "select": {
            let arg0, arg1;
            if (args.length !== 2 || !(arg0 = getStringLiteralLike(args[0])) || !(arg1 = getStringLiteralLike(args[1]))) {
                console.error(`Expected two string literal arguments in goTo.select, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            return [{
                kind: "goTo",
                funcName: "select",
                args: [getGoStringLiteral(arg0.text), getGoStringLiteral(arg1.text)],
            }];
        }
        default:
            console.error(`Unrecognized goTo function: ${funcName}`);
            return undefined;
    }
}

function parseVerifyCompletionsArgs(args: readonly ts.Expression[], codeActionArgs?: readonly ts.Expression[]): VerifyCompletionsCmd[] | undefined {
    const cmds = [];
    const codeAction = codeActionArgs?.[0] && parseAndApplyCodeActionArg(codeActionArgs[0]);
    for (const arg of args) {
        const result = parseVerifyCompletionArg(arg, codeAction);
        if (!result) {
            return undefined;
        }
        if (codeActionArgs?.length) {
            result.andApplyCodeActionArgs = parseAndApplyCodeActionArg(codeActionArgs[0]);
        }
        cmds.push(result);
    }
    return cmds;
}

function parseVerifyApplyCodeActionFromCompletionArgs(args: readonly ts.Expression[]): VerifyApplyCodeActionFromCompletionCmd[] | undefined {
    const cmds: VerifyApplyCodeActionFromCompletionCmd[] = [];
    if (args.length !== 2) {
        console.error(`Expected two arguments in verify.applyCodeActionFromCompletion, got ${args.map(arg => arg.getText()).join(", ")}`);
        return undefined;
    }
    if (!ts.isStringLiteralLike(args[0]) && args[0].getText() !== "undefined") {
        console.error(`Expected string literal or "undefined" in verify.applyCodeActionFromCompletion, got ${args[0].getText()}`);
        return undefined;
    }
    const markerName = getStringLiteralLike(args[0])?.text;
    const marker = markerName === undefined ? "nil" : `PtrTo(${getGoStringLiteral(markerName)})`;
    const options = parseVerifyApplyCodeActionArgs(args[1]);
    if (options === undefined) {
        return undefined;
    }

    cmds.push({ kind: "verifyApplyCodeActionFromCompletion", marker, options });
    return cmds;
}

function parseVerifyApplyCodeActionArgs(arg: ts.Expression): string | undefined {
    const obj = getObjectLiteralExpression(arg);
    if (!obj) {
        console.error(`Expected object literal for verify.applyCodeActionFromCompletion options, got ${arg.getText()}`);
        return undefined;
    }
    let nameInit, sourceInit, descInit, dataInit;
    const props: string[] = [];
    for (const prop of obj.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === "preferences") {
                continue; // !!! parse once preferences are supported in fourslash
            }
            console.error(`Expected property assignment with identifier name in verify.applyCodeActionFromCompletion options, got ${prop.getText()}`);
            return undefined;
        }
        const propName = prop.name.text;
        const init = prop.initializer;
        switch (propName) {
            case "name":
                nameInit = getStringLiteralLike(init);
                if (!nameInit) {
                    console.error(`Expected string literal for name in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
                    return undefined;
                }
                props.push(`Name: ${getGoStringLiteral(nameInit.text)},`);
                break;
            case "source":
                sourceInit = getStringLiteralLike(init);
                if (!sourceInit) {
                    console.error(`Expected string literal for source in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
                    return undefined;
                }
                props.push(`Source: ${getGoStringLiteral(sourceInit.text)},`);
                break;
            case "data":
                dataInit = getObjectLiteralExpression(init);
                if (!dataInit) {
                    console.error(`Expected object literal for data in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
                    return undefined;
                }
                const dataProps: string[] = [];
                for (const dataProp of dataInit.properties) {
                    if (!ts.isPropertyAssignment(dataProp) || !ts.isIdentifier(dataProp.name)) {
                        console.error(`Expected property assignment with identifier name in verify.applyCodeActionFromCompletion data, got ${dataProp.getText()}`);
                        return undefined;
                    }
                    const dataPropName = dataProp.name.text;
                    switch (dataPropName) {
                        case "moduleSpecifier":
                            const moduleSpecifierInit = getStringLiteralLike(dataProp.initializer);
                            if (!moduleSpecifierInit) {
                                console.error(`Expected string literal for moduleSpecifier in verify.applyCodeActionFromCompletion data, got ${dataProp.initializer.getText()}`);
                                return undefined;
                            }
                            dataProps.push(`ModuleSpecifier: ${getGoStringLiteral(moduleSpecifierInit.text)},`);
                            break;
                        case "exportName":
                            const exportNameInit = getStringLiteralLike(dataProp.initializer);
                            if (!exportNameInit) {
                                console.error(`Expected string literal for exportName in verify.applyCodeActionFromCompletion data, got ${dataProp.initializer.getText()}`);
                                return undefined;
                            }
                            dataProps.push(`ExportName: ${getGoStringLiteral(exportNameInit.text)},`);
                            break;
                        case "fileName":
                            const fileNameInit = getStringLiteralLike(dataProp.initializer);
                            if (!fileNameInit) {
                                console.error(`Expected string literal for fileName in verify.applyCodeActionFromCompletion data, got ${dataProp.initializer.getText()}`);
                                return undefined;
                            }
                            dataProps.push(`FileName: ${getGoStringLiteral(fileNameInit.text)},`);
                            break;
                        default:
                            console.error(`Unrecognized property in verify.applyCodeActionFromCompletion data: ${dataProp.getText()}`);
                            return undefined;
                    }
                }
                props.push(`AutoImportData: &lsproto.AutoImportData{\n${dataProps.join("\n")}\n},`);
                break;
            case "description":
                descInit = getStringLiteralLike(init);
                if (!descInit) {
                    console.error(`Expected string literal for description in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
                    return undefined;
                }
                props.push(`Description: ${getGoStringLiteral(descInit.text)},`);
                break;
            case "newFileContent":
                const newFileContentInit = getStringLiteralLike(init);
                if (!newFileContentInit) {
                    console.error(`Expected string literal for newFileContent in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
                    return undefined;
                }
                props.push(`NewFileContent: PtrTo(${getGoMultiLineStringLiteral(newFileContentInit.text)}),`);
                break;
            case "newRangeContent":
                const newRangeContentInit = getStringLiteralLike(init);
                if (!newRangeContentInit) {
                    console.error(`Expected string literal for newRangeContent in verify.applyCodeActionFromCompletion options, got ${init.getText()}`);
                    return undefined;
                }
                props.push(`NewRangeContent: PtrTo(${getGoMultiLineStringLiteral(newRangeContentInit.text)}),`);
                break;
            case "preferences":
                // Few if any tests use non-default preferences
                break;
            default:
                console.error(`Unrecognized property in verify.applyCodeActionFromCompletion options: ${prop.getText()}`);
                return undefined;
        }
    }
    if (!nameInit) {
        console.error(`Expected name property in verify.applyCodeActionFromCompletion options`);
        return undefined;
    }
    if (!sourceInit && !dataInit) {
        console.error(`Expected source property in verify.applyCodeActionFromCompletion options`);
        return undefined;
    }
    if (!descInit) {
        console.error(`Expected description property in verify.applyCodeActionFromCompletion options`);
        return undefined;
    }
    return `&fourslash.ApplyCodeActionFromCompletionOptions{\n${props.join("\n")}\n}`;
}

function parseImportFixAtPositionArgs(args: readonly ts.Expression[]): VerifyImportFixAtPositionCmd[] | undefined {
    if (args.length < 1 || args.length > 3) {
        console.error(`Expected 1-3 arguments in verify.importFixAtPosition, got ${args.map(arg => arg.getText()).join(", ")}`);
        return undefined;
    }
    const arrayArg = getArrayLiteralExpression(args[0]);
    if (!arrayArg) {
        console.error(`Expected array literal for first argument in verify.importFixAtPosition, got ${args[0].getText()}`);
        return undefined;
    }
    const expectedTexts: string[] = [];
    for (const elem of arrayArg.elements) {
        const strElem = getStringLiteralLike(elem);
        if (!strElem) {
            console.error(`Expected string literal in verify.importFixAtPosition array, got ${elem.getText()}`);
            return undefined;
        }
        expectedTexts.push(getGoMultiLineStringLiteral(strElem.text));
    }

    // If the array is empty, we should still generate valid Go code
    if (expectedTexts.length === 0) {
        expectedTexts.push(""); // This will be handled specially in code generation
    }

    let preferences: string | undefined;
    if (args.length > 2 && ts.isObjectLiteralExpression(args[2])) {
        preferences = parseUserPreferences(args[2]);
        if (!preferences) {
            console.error(`Unrecognized user preferences in verify.importFixAtPosition: ${args[2].getText()}`);
            return undefined;
        }
    }
    return [{
        kind: "verifyImportFixAtPosition",
        expectedTexts,
        preferences: preferences || "nil /*preferences*/",
    }];
}

const completionConstants = new Map([
    ["completion.globals", "CompletionGlobals"],
    ["completion.globalTypes", "CompletionGlobalTypes"],
    ["completion.classElementKeywords", "CompletionClassElementKeywords"],
    ["completion.classElementInJsKeywords", "CompletionClassElementInJSKeywords"],
    ["completion.constructorParameterKeywords", "CompletionConstructorParameterKeywords"],
    ["completion.functionMembersWithPrototype", "CompletionFunctionMembersWithPrototype"],
    ["completion.functionMembers", "CompletionFunctionMembers"],
    ["completion.typeKeywords", "CompletionTypeKeywords"],
    ["completion.undefinedVarEntry", "CompletionUndefinedVarItem"],
    ["completion.typeAssertionKeywords", "CompletionTypeAssertionKeywords"],
    ["completion.globalThisEntry", "CompletionGlobalThisItem"],
]);

const completionPlus = new Map([
    ["completion.globalsPlus", "CompletionGlobalsPlus"],
    ["completion.globalTypesPlus", "CompletionGlobalTypesPlus"],
    ["completion.functionMembersPlus", "CompletionFunctionMembersPlus"],
    ["completion.functionMembersWithPrototypePlus", "CompletionFunctionMembersWithPrototypePlus"],
    ["completion.globalsInJsPlus", "CompletionGlobalsInJSPlus"],
    ["completion.typeKeywordsPlus", "CompletionTypeKeywordsPlus"],
]);

function parseVerifyCompletionArg(arg: ts.Expression, codeActionArgs?: VerifyApplyCodeActionArgs): VerifyCompletionsCmd | undefined {
    let marker: string | undefined;
    let goArgs: VerifyCompletionsArgs | undefined;
    const obj = getObjectLiteralExpression(arg);
    if (!obj) {
        console.error(`Expected object literal expression in verify.completions, got ${arg.getText()}`);
        return undefined;
    }
    let isNewIdentifierLocation: true | undefined;
    for (const prop of obj.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === "preferences") {
                continue; // !!! parse once preferences are supported in fourslash
            }
            console.error(`Expected property assignment with identifier name, got ${prop.getText()}`);
            return undefined;
        }
        const propName = prop.name.text;
        const init = prop.initializer;
        switch (propName) {
            case "marker": {
                let markerInit;
                if (markerInit = getStringLiteralLike(init)) {
                    marker = getGoStringLiteral(markerInit.text);
                }
                else if (markerInit = getArrayLiteralExpression(init)) {
                    marker = "[]string{";
                    for (const elem of markerInit.elements) {
                        if (!ts.isStringLiteral(elem)) {
                            console.error(`Expected string literal in marker array, got ${elem.getText()}`);
                            return undefined; // !!! parse marker arrays?
                        }
                        marker += `${getGoStringLiteral(elem.text)}, `;
                    }
                    marker += "}";
                }
                else if (markerInit = getObjectLiteralExpression(init)) {
                    // !!! parse marker objects?
                    console.error(`Unrecognized marker initializer: ${markerInit.getText()}`);
                    return undefined;
                }
                else if (init.getText() === "test.markers()") {
                    marker = "f.Markers()";
                }
                else if (
                    ts.isCallExpression(init)
                    && init.expression.getText() === "test.marker"
                    && ts.isStringLiteralLike(init.arguments[0])
                ) {
                    marker = getGoStringLiteral(init.arguments[0].text);
                }
                else {
                    console.error(`Unrecognized marker initializer: ${init.getText()}`);
                    return undefined;
                }
                break;
            }
            case "exact":
            case "includes":
            case "unsorted": {
                if (init.getText() === "undefined") {
                    return {
                        kind: "verifyCompletions",
                        marker: marker ? marker : "nil",
                        args: "nil",
                    };
                }
                let expected: string;
                const initText = init.getText();
                if (completionConstants.has(initText)) {
                    expected = completionConstants.get(initText)!;
                }
                else if (completionPlus.keys().some(funcName => initText.startsWith(funcName))) {
                    const tsFunc = completionPlus.keys().find(funcName => initText.startsWith(funcName));
                    const funcName = completionPlus.get(tsFunc!)!;
                    const maybeItems = (init as ts.CallExpression).arguments[0];
                    const maybeOpts = (init as ts.CallExpression).arguments[1];
                    let items;
                    if (!(items = getArrayLiteralExpression(maybeItems))) {
                        console.error(`Expected array literal expression for completion.globalsPlus items, got ${maybeItems.getText()}`);
                        return undefined;
                    }
                    expected = `${funcName}(\n[]fourslash.CompletionsExpectedItem{`;
                    for (const elem of items.elements) {
                        const result = parseExpectedCompletionItem(elem, codeActionArgs);
                        if (!result) {
                            return undefined;
                        }
                        expected += "\n" + result + ",";
                    }
                    expected += "\n}";
                    if (maybeOpts) {
                        let opts;
                        if (!(opts = getObjectLiteralExpression(maybeOpts))) {
                            console.error(`Expected object literal expression for completion.globalsPlus options, got ${maybeOpts.getText()}`);
                            return undefined;
                        }
                        const noLib = opts.properties[0];
                        if (noLib && ts.isPropertyAssignment(noLib) && noLib.name.getText() === "noLib") {
                            if (noLib.initializer.kind === ts.SyntaxKind.TrueKeyword) {
                                expected += ", true";
                            }
                            else if (noLib.initializer.kind === ts.SyntaxKind.FalseKeyword) {
                                expected += ", false";
                            }
                            else {
                                console.error(`Expected boolean literal for noLib, got ${noLib.initializer.getText()}`);
                                return undefined;
                            }
                        }
                        else {
                            console.error(`Expected noLib property in completion.globalsPlus options, got ${maybeOpts.getText()}`);
                            return undefined;
                        }
                    }
                    else if (tsFunc === "completion.globalsPlus" || tsFunc === "completion.globalsInJsPlus") {
                        expected += ", false"; // Default for noLib
                    }
                    expected += ")";
                }
                else {
                    expected = "[]fourslash.CompletionsExpectedItem{";
                    let items;
                    if (items = getArrayLiteralExpression(init)) {
                        for (const elem of items.elements) {
                            const result = parseExpectedCompletionItem(elem);
                            if (!result) {
                                return undefined;
                            }
                            expected += "\n" + result + ",";
                        }
                    }
                    else {
                        const result = parseExpectedCompletionItem(init);
                        if (!result) {
                            return undefined;
                        }
                        expected += "\n" + result + ",";
                    }
                    expected += "\n}";
                }
                if (propName === "includes") {
                    (goArgs ??= {}).includes = expected;
                }
                else if (propName === "exact") {
                    (goArgs ??= {}).exact = expected;
                }
                else {
                    (goArgs ??= {}).unsorted = expected;
                }
                break;
            }
            case "excludes": {
                let excludes = "[]string{";
                let item;
                if (item = getStringLiteralLike(init)) {
                    excludes += `\n${getGoStringLiteral(item.text)},`;
                }
                else if (item = getArrayLiteralExpression(init)) {
                    for (const elem of item.elements) {
                        if (!ts.isStringLiteral(elem)) {
                            return undefined; // Shouldn't happen
                        }
                        excludes += `\n${getGoStringLiteral(elem.text)},`;
                    }
                }
                excludes += "\n}";
                (goArgs ??= {}).excludes = excludes;
                break;
            }
            case "isNewIdentifierLocation":
                if (init.kind === ts.SyntaxKind.TrueKeyword) {
                    isNewIdentifierLocation = true;
                }
                break;
            case "preferences":
            case "triggerCharacter":
                break; // !!! parse once they're supported in fourslash
            case "defaultCommitCharacters":
            case "optionalReplacementSpan": // the only two tests that use this will require manual conversion
            case "isGlobalCompletion":
                break; // Ignored, unused
            default:
                console.error(`Unrecognized expected completion item: ${init.parent.getText()}`);
                return undefined;
        }
    }
    return {
        kind: "verifyCompletions",
        marker: marker ? marker : "nil",
        args: goArgs,
        isNewIdentifierLocation: isNewIdentifierLocation,
    };
}

function parseExpectedCompletionItem(expr: ts.Expression, codeActionArgs?: VerifyApplyCodeActionArgs): string | undefined {
    if (completionConstants.has(expr.getText())) {
        return completionConstants.get(expr.getText())!;
    }
    let strExpr;
    if (strExpr = getStringLiteralLike(expr)) {
        return getGoStringLiteral(strExpr.text);
    }
    if (strExpr = getObjectLiteralExpression(expr)) {
        let isDeprecated = false; // !!!
        let isOptional = false;
        let sourceInit: ts.StringLiteralLike | undefined;
        let extensions: string[] = []; // !!!
        let itemProps: string[] = [];
        let name: string | undefined;
        let insertText: string | undefined;
        let filterText: string | undefined;
        let replacementSpanIdx: string | undefined;
        for (const prop of strExpr.properties) {
            if (!(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) || !ts.isIdentifier(prop.name)) {
                console.error(`Expected property assignment with identifier name for completion item, got ${prop.getText()}`);
                return undefined;
            }
            const propName = prop.name.text;
            const init = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
            switch (propName) {
                case "name": {
                    let nameInit;
                    if (nameInit = getStringLiteralLike(init)) {
                        name = nameInit.text;
                    }
                    else {
                        console.error(`Expected string literal for completion item name, got ${init.getText()}`);
                        return undefined;
                    }
                    break;
                }
                case "sortText":
                    const result = parseSortText(init);
                    if (!result) {
                        return undefined;
                    }
                    itemProps.push(`SortText: PtrTo(string(${result})),`);
                    if (result === "ls.SortTextOptionalMember") {
                        isOptional = true;
                    }
                    break;
                case "insertText": {
                    let insertTextInit;
                    if (insertTextInit = getStringLiteralLike(init)) {
                        insertText = insertTextInit.text;
                    }
                    else if (init.getText() === "undefined") {
                        // Ignore
                    }
                    else {
                        console.error(`Expected string literal for insertText, got ${init.getText()}`);
                        return undefined;
                    }
                    break;
                }
                case "filterText": {
                    let filterTextInit;
                    if (filterTextInit = getStringLiteralLike(init)) {
                        filterText = filterTextInit.text;
                    }
                    else {
                        console.error(`Expected string literal for filterText, got ${init.getText()}`);
                        return undefined;
                    }
                    break;
                }
                case "isRecommended":
                    if (init.kind === ts.SyntaxKind.TrueKeyword) {
                        itemProps.push(`Preselect: PtrTo(true),`);
                    }
                    break;
                case "kind":
                    const kind = parseKind(init);
                    if (!kind) {
                        return undefined;
                    }
                    itemProps.push(`Kind: PtrTo(${kind}),`);
                    break;
                case "kindModifiers":
                    const modifiers = parseKindModifiers(init);
                    if (!modifiers) {
                        return undefined;
                    }
                    ({ isDeprecated, isOptional, extensions } = modifiers);
                    break;
                case "text": {
                    let textInit;
                    if (textInit = getStringLiteralLike(init)) {
                        itemProps.push(`Detail: PtrTo(${getGoStringLiteral(textInit.text)}),`);
                    }
                    else {
                        console.error(`Expected string literal for text, got ${init.getText()}`);
                        return undefined;
                    }
                    break;
                }
                case "documentation": {
                    let docInit;
                    if (docInit = getStringLiteralLike(init)) {
                        itemProps.push(`Documentation: &lsproto.StringOrMarkupContent{
						MarkupContent: &lsproto.MarkupContent{
							Kind:  lsproto.MarkupKindMarkdown,
							Value: ${getGoStringLiteral(docInit.text)},
						},
					},`);
                    }
                    else {
                        console.error(`Expected string literal for documentation, got ${init.getText()}`);
                        return undefined;
                    }
                    break;
                }
                case "isFromUncheckedFile":
                    break; // Ignored
                case "hasAction":
                    itemProps.push("AdditionalTextEdits: fourslash.AnyTextEdits,");
                    break;
                case "source":
                case "sourceDisplay":
                    if (sourceInit !== undefined) {
                        break;
                    }
                    if (sourceInit = getStringLiteralLike(init)) {
                        if (propName === "source" && sourceInit.text.endsWith("/")) {
                            // source: "ClassMemberSnippet/"
                            itemProps.push(`Data: &lsproto.CompletionItemData{
                                Source: ${getGoStringLiteral(sourceInit.text)},
                            },`);
                            break;
                        }
                        itemProps.push(`Data: &lsproto.CompletionItemData{
                            AutoImport: &lsproto.AutoImportData{
                                ModuleSpecifier: ${getGoStringLiteral(sourceInit.text)},
                            },
                        },`);
                    }
                    else {
                        console.error(`Expected string literal for source/sourceDisplay, got ${init.getText()}`);
                        return undefined;
                    }
                    break;
                case "commitCharacters":
                    // !!! support these later
                    break;
                case "replacementSpan": {
                    let span;
                    if (ts.isIdentifier(init)) {
                        span = getNodeOfKind(init, (n: ts.Node): n is ts.Node => !ts.isIdentifier(n));
                    }
                    else {
                        span = init;
                    }
                    if (span?.getText().startsWith("test.ranges()[")) {
                        replacementSpanIdx = span.getText().match(/\d+/)?.[0];
                    }
                    break;
                }
                default:
                    console.error(`Unrecognized property in expected completion item: ${propName}`);
                    return undefined; // Unsupported property
            }
        }
        if (!name) {
            return undefined; // Shouldn't happen
        }
        if (codeActionArgs && codeActionArgs.name === name && codeActionArgs.source === sourceInit?.text) {
            itemProps.push(`LabelDetails: &lsproto.CompletionItemLabelDetails{
                Description: PtrTo(${getGoStringLiteral(codeActionArgs.source)}),
            },`);
        }
        if (replacementSpanIdx) {
            itemProps.push(`TextEdit: &lsproto.TextEditOrInsertReplaceEdit{
                TextEdit: &lsproto.TextEdit{
                    NewText: ${getGoStringLiteral(name)},
                    Range:   f.Ranges()[${replacementSpanIdx}].LSRange,
                },
            },`);
        }
        if (isOptional) {
            insertText ??= name;
            filterText ??= name;
            name += "?";
        }
        if (filterText) itemProps.unshift(`FilterText: PtrTo(${getGoStringLiteral(filterText)}),`);
        if (insertText) itemProps.unshift(`InsertText: PtrTo(${getGoStringLiteral(insertText)}),`);
        itemProps.unshift(`Label: ${getGoStringLiteral(name!)},`);
        return `&lsproto.CompletionItem{\n${itemProps.join("\n")}}`;
    }
    console.error(`Expected string literal or object literal for expected completion item, got ${expr.getText()}`);
    return undefined; // Unsupported expression type
}

function parseAndApplyCodeActionArg(arg: ts.Expression): VerifyApplyCodeActionArgs | undefined {
    const obj = getObjectLiteralExpression(arg);
    if (!obj) {
        console.error(`Expected object literal for code action argument, got ${arg.getText()}`);
        return undefined;
    }
    const nameProperty = obj.properties.find(prop =>
        ts.isPropertyAssignment(prop) &&
        ts.isIdentifier(prop.name) &&
        prop.name.text === "name" &&
        ts.isStringLiteralLike(prop.initializer)
    ) as ts.PropertyAssignment | undefined;
    if (!nameProperty) {
        console.error(`Expected name property in code action argument, got ${obj.getText()}`);
        return undefined;
    }
    const sourceProperty = obj.properties.find(prop =>
        ts.isPropertyAssignment(prop) &&
        ts.isIdentifier(prop.name) &&
        prop.name.text === "source" &&
        ts.isStringLiteralLike(prop.initializer)
    ) as ts.PropertyAssignment | undefined;
    if (!sourceProperty) {
        console.error(`Expected source property in code action argument, got ${obj.getText()}`);
        return undefined;
    }
    const descriptionProperty = obj.properties.find(prop =>
        ts.isPropertyAssignment(prop) &&
        ts.isIdentifier(prop.name) &&
        prop.name.text === "description" &&
        ts.isStringLiteralLike(prop.initializer)
    ) as ts.PropertyAssignment | undefined;
    if (!descriptionProperty) {
        console.error(`Expected description property in code action argument, got ${obj.getText()}`);
        return undefined;
    }
    const newFileContentProperty = obj.properties.find(prop =>
        ts.isPropertyAssignment(prop) &&
        ts.isIdentifier(prop.name) &&
        prop.name.text === "newFileContent" &&
        ts.isStringLiteralLike(prop.initializer)
    ) as ts.PropertyAssignment | undefined;
    if (!newFileContentProperty) {
        console.error(`Expected newFileContent property in code action argument, got ${obj.getText()}`);
        return undefined;
    }
    return {
        name: (nameProperty.initializer as ts.StringLiteralLike).text,
        source: (sourceProperty.initializer as ts.StringLiteralLike).text,
        description: (descriptionProperty.initializer as ts.StringLiteralLike).text,
        newFileContent: (newFileContentProperty.initializer as ts.StringLiteralLike).text,
    };
}

function parseBaselineFindAllReferencesArgs(args: readonly ts.Expression[]): [VerifyBaselineFindAllReferencesCmd] | undefined {
    const newArgs = [];
    for (const arg of args) {
        let strArg;
        if (strArg = getStringLiteralLike(arg)) {
            newArgs.push(getGoStringLiteral(strArg.text));
        }
        else if (arg.getText() === "...test.markerNames()") {
            newArgs.push("f.MarkerNames()...");
        }
        else if (arg.getText() === "...test.ranges()") {
            return [{
                kind: "verifyBaselineFindAllReferences",
                markers: [],
                ranges: true,
            }];
        }
        else {
            console.error(`Unrecognized argument in verify.baselineFindAllReferences: ${arg.getText()}`);
            return undefined;
        }
    }

    return [{
        kind: "verifyBaselineFindAllReferences",
        markers: newArgs,
    }];
}

function parseBaselineDocumentHighlightsArgs(args: readonly ts.Expression[]): [VerifyBaselineDocumentHighlightsCmd] | undefined {
    const newArgs: string[] = [];
    let preferences: string | undefined;
    for (const arg of args) {
        let strArg;
        if (strArg = getArrayLiteralExpression(arg)) {
            for (const elem of strArg.elements) {
                const newArg = parseBaselineMarkerOrRangeArg(elem);
                if (!newArg) {
                    return undefined;
                }
                newArgs.push(newArg);
            }
        }
        else if (ts.isObjectLiteralExpression(arg)) {
            // !!! todo when multiple files supported in lsp
        }
        else if (strArg = parseBaselineMarkerOrRangeArg(arg)) {
            newArgs.push(strArg);
        }
        else {
            console.error(`Unrecognized argument in verify.baselineDocumentHighlights: ${arg.getText()}`);
            return undefined;
        }
    }

    if (newArgs.length === 0) {
        newArgs.push("ToAny(f.Ranges())...");
    }

    return [{
        kind: "verifyBaselineDocumentHighlights",
        args: newArgs,
        preferences: preferences ? preferences : "nil /*preferences*/",
    }];
}

function parseBaselineGoToDefinitionArgs(
    funcName: "baselineGoToDefinition" | "baselineGoToType" | "baselineGetDefinitionAtPosition" | "baselineGoToImplementation",
    args: readonly ts.Expression[],
): [VerifyBaselineGoToDefinitionCmd] | undefined {
    let boundSpan: true | undefined;
    if (funcName === "baselineGoToDefinition") {
        boundSpan = true;
    }
    let kind: "verifyBaselineGoToDefinition" | "verifyBaselineGoToType" | "verifyBaselineGoToImplementation";
    switch (funcName) {
        case "baselineGoToDefinition":
        case "baselineGetDefinitionAtPosition":
            kind = "verifyBaselineGoToDefinition";
            break;
        case "baselineGoToType":
            kind = "verifyBaselineGoToType";
            break;
        case "baselineGoToImplementation":
            kind = "verifyBaselineGoToImplementation";
            break;
    }
    const newArgs = [];
    for (const arg of args) {
        let strArg;
        if (strArg = getStringLiteralLike(arg)) {
            newArgs.push(getGoStringLiteral(strArg.text));
        }
        else if (arg.getText() === "...test.markerNames()") {
            newArgs.push("f.MarkerNames()...");
        }
        else if (arg.getText() === "...test.ranges()") {
            return [{
                kind,
                markers: [],
                ranges: true,
                boundSpan,
            }];
        }
        else {
            console.error(`Unrecognized argument in verify.${funcName}: ${arg.getText()}`);
            return undefined;
        }
    }

    return [{
        kind,
        markers: newArgs,
        boundSpan,
    }];
}

function parseRenameInfo(funcName: "renameInfoSucceeded" | "renameInfoFailed", args: readonly ts.Expression[]): [VerifyRenameInfoCmd] | undefined {
    let preferences = "nil /*preferences*/";
    let prefArg;
    switch (funcName) {
        case "renameInfoSucceeded":
            if (args[6]) {
                prefArg = args[6];
            }
            break;
        case "renameInfoFailed":
            if (args[1]) {
                prefArg = args[1];
            }
            break;
    }
    if (prefArg) {
        if (!ts.isObjectLiteralExpression(prefArg)) {
            console.error(`Expected object literal expression for preferences, got ${prefArg.getText()}`);
            return undefined;
        }
        const parsedPreferences = parseUserPreferences(prefArg);
        if (!parsedPreferences) {
            console.error(`Unrecognized user preferences in ${funcName}: ${prefArg.getText()}`);
            return undefined;
        }
    }
    return [{ kind: funcName, preferences }];
}

function parseBaselineRenameArgs(funcName: string, args: readonly ts.Expression[]): [VerifyBaselineRenameCmd] | undefined {
    let newArgs: string[] = [];
    let preferences: string | undefined;
    for (const arg of args) {
        let typedArg;
        if ((typedArg = getArrayLiteralExpression(arg))) {
            for (const elem of typedArg.elements) {
                const newArg = parseBaselineMarkerOrRangeArg(elem);
                if (!newArg) {
                    return undefined;
                }
                newArgs.push(newArg);
            }
        }
        else if (ts.isObjectLiteralExpression(arg)) {
            preferences = parseUserPreferences(arg);
            if (!preferences) {
                console.error(`Unrecognized user preferences in verify.baselineRename: ${arg.getText()}`);
                return undefined;
            }
            continue;
        }
        else if (typedArg = parseBaselineMarkerOrRangeArg(arg)) {
            newArgs.push(typedArg);
        }
        else {
            return undefined;
        }
    }
    return [{
        kind: funcName === "baselineRenameAtRangesWithText" ? "verifyBaselineRenameAtRangesWithText" : "verifyBaselineRename",
        args: newArgs,
        preferences: preferences ? preferences : "nil /*preferences*/",
    }];
}

function parseBaselineInlayHints(args: readonly ts.Expression[]): [VerifyBaselineInlayHintsCmd] | undefined {
    let preferences: string | undefined;
    // Parse span
    if (args.length > 0) {
        if (args[0].getText() !== "undefined") {
            console.error(`Unsupported span argument in verify.baselineInlayHints: ${args[0].getText()}`);
            return undefined;
        }
    }
    // Parse preferences
    if (args.length > 1) {
        if (ts.isObjectLiteralExpression(args[1])) {
            preferences = parseUserPreferences(args[1]);
            if (!preferences) {
                console.error(`Unrecognized user preferences in verify.baselineInlayHints: ${args[1].getText()}`);
                return undefined;
            }
        }
    }
    return [{
        kind: "verifyBaselineInlayHints",
        span: "nil /*span*/", // Only supporteed manually
        preferences: preferences ? preferences : "nil /*preferences*/",
    }];
}

function parseVerifyDiagnostics(funcName: string, args: readonly ts.Expression[]): [VerifyDiagnosticsCmd] | undefined {
    if (!args[0] || !ts.isArrayLiteralExpression(args[0])) {
        console.error(`Expected an array literal argument in verify.${funcName}`);
        return undefined;
    }
    const goArgs: string[] = [];
    for (const expr of args[0].elements) {
        const diag = parseExpectedDiagnostic(expr);
        if (diag === undefined) {
            return undefined;
        }
        goArgs.push(diag);
    }
    return [{
        kind: "verifyDiagnostics",
        arg: goArgs.length > 0 ? `[]*lsproto.Diagnostic{\n${goArgs.join(",\n")},\n}` : "nil",
        isSuggestion: funcName === "getSuggestionDiagnostics",
    }];
}

function parseExpectedDiagnostic(expr: ts.Expression): string | undefined {
    if (!ts.isObjectLiteralExpression(expr)) {
        console.error(`Expected object literal expression for expected diagnostic, got ${expr.getText()}`);
        return undefined;
    }

    const diagnosticProps: string[] = [];

    for (const prop of expr.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            console.error(`Expected property assignment with identifier name for expected diagnostic, got ${prop.getText()}`);
            return undefined;
        }

        const propName = prop.name.text;
        const init = prop.initializer;

        switch (propName) {
            case "message": {
                let messageInit;
                if (messageInit = getStringLiteralLike(init)) {
                    messageInit.text = messageInit.text.replace("/tests/cases/fourslash", "");
                    diagnosticProps.push(`Message: ${getGoStringLiteral(messageInit.text)},`);
                }
                else {
                    console.error(`Expected string literal for diagnostic message, got ${init.getText()}`);
                    return undefined;
                }
                break;
            }
            case "code": {
                let codeInit;
                if (codeInit = getNumericLiteral(init)) {
                    diagnosticProps.push(`Code: &lsproto.IntegerOrString{Integer: PtrTo[int32](${codeInit.text})},`);
                }
                else {
                    console.error(`Expected numeric literal for diagnostic code, got ${init.getText()}`);
                    return undefined;
                }
                break;
            }
            case "range": {
                // Handle range references like ranges[0]
                const rangeArg = parseBaselineMarkerOrRangeArg(init);
                if (rangeArg) {
                    diagnosticProps.push(`Range: ${rangeArg}.LSRange,`);
                }
                else {
                    console.error(`Expected range reference for diagnostic range, got ${init.getText()}`);
                    return undefined;
                }
                break;
            }
            case "reportsDeprecated": {
                if (init.kind === ts.SyntaxKind.TrueKeyword) {
                    diagnosticProps.push(`Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated},`);
                }
                break;
            }
            case "reportsUnnecessary": {
                if (init.kind === ts.SyntaxKind.TrueKeyword) {
                    diagnosticProps.push(`Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagUnnecessary},`);
                }
                break;
            }
            default:
                console.error(`Unrecognized property in expected diagnostic: ${propName}`);
                return undefined;
        }
    }

    if (diagnosticProps.length === 0) {
        console.error(`No valid properties found in diagnostic object`);
        return undefined;
    }

    return `&lsproto.Diagnostic{\n${diagnosticProps.join("\n")}\n}`;
}

function stringToTristate(s: string): string {
    switch (s) {
        case "true":
            return "core.TSTrue";
        case "false":
            return "core.TSFalse";
        default:
            return "core.TSUnknown";
    }
}

function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefined {
    const inlayHintPreferences: string[] = [];
    const preferences: string[] = [];
    for (const prop of arg.properties) {
        if (ts.isPropertyAssignment(prop)) {
            switch (prop.name.getText()) {
                // !!! other preferences
                case "providePrefixAndSuffixTextForRename":
                    preferences.push(`UseAliasesForRename: ${stringToTristate(prop.initializer.getText())}`);
                    break;
                case "quotePreference":
                    preferences.push(`QuotePreference: lsutil.QuotePreference(${prop.initializer.getText()})`);
                    break;
                case "autoImportFileExcludePatterns":
                    const arrayArg = getArrayLiteralExpression(prop.initializer);
                    if (!arrayArg) {
                        return undefined;
                    }
                    const patterns: string[] = [];
                    for (const elem of arrayArg.elements) {
                        const strElem = getStringLiteralLike(elem);
                        if (!strElem) {
                            return undefined;
                        }
                        patterns.push(getGoStringLiteral(strElem.text));
                    }
                    preferences.push(`AutoImportFileExcludePatterns: []string{${patterns.join(", ")}}`);
                    break;
                case "includeInlayParameterNameHints":
                    let paramHint;
                    if (!ts.isStringLiteralLike(prop.initializer)) {
                        return undefined;
                    }
                    switch (prop.initializer.text) {
                        case "none":
                            paramHint = "lsutil.IncludeInlayParameterNameHintsNone";
                            break;
                        case "literals":
                            paramHint = "lsutil.IncludeInlayParameterNameHintsLiterals";
                            break;
                        case "all":
                            paramHint = "lsutil.IncludeInlayParameterNameHintsAll";
                            break;
                    }
                    inlayHintPreferences.push(`IncludeInlayParameterNameHints: ${paramHint}`);
                    break;
                case "includeInlayParameterNameHintsWhenArgumentMatchesName":
                    inlayHintPreferences.push(`IncludeInlayParameterNameHintsWhenArgumentMatchesName: ${prop.initializer.getText()}`);
                    break;
                case "includeInlayFunctionParameterTypeHints":
                    inlayHintPreferences.push(`IncludeInlayFunctionParameterTypeHints: ${prop.initializer.getText()}`);
                    break;
                case "includeInlayVariableTypeHints":
                    inlayHintPreferences.push(`IncludeInlayVariableTypeHints: ${prop.initializer.getText()}`);
                    break;
                case "includeInlayVariableTypeHintsWhenTypeMatchesName":
                    inlayHintPreferences.push(`IncludeInlayVariableTypeHintsWhenTypeMatchesName: ${prop.initializer.getText()}`);
                    break;
                case "includeInlayPropertyDeclarationTypeHints":
                    inlayHintPreferences.push(`IncludeInlayPropertyDeclarationTypeHints: ${prop.initializer.getText()}`);
                    break;
                case "includeInlayFunctionLikeReturnTypeHints":
                    inlayHintPreferences.push(`IncludeInlayFunctionLikeReturnTypeHints: ${prop.initializer.getText()}`);
                    break;
                case "includeInlayEnumMemberValueHints":
                    inlayHintPreferences.push(`IncludeInlayEnumMemberValueHints: ${prop.initializer.getText()}`);
                    break;
                case "interactiveInlayHints":
                    // Ignore, deprecated
                    break;
            }
        }
        else {
            return undefined;
        }
    }

    if (inlayHintPreferences.length > 0) {
        preferences.push(`InlayHints: lsutil.InlayHintsPreferences{${inlayHintPreferences.join(",")}}`);
    }
    if (preferences.length === 0) {
        return "nil /*preferences*/";
    }
    return `&lsutil.UserPreferences{${preferences.join(",")}}`;
}

function parseBaselineMarkerOrRangeArg(arg: ts.Expression): string | undefined {
    if (ts.isStringLiteral(arg)) {
        return getGoStringLiteral(arg.text);
    }
    else if (ts.isIdentifier(arg) || (ts.isElementAccessExpression(arg) && ts.isIdentifier(arg.expression))) {
        const result = parseRangeVariable(arg);
        if (result) {
            return result;
        }
        const init = getNodeOfKind(arg, ts.isCallExpression);
        if (init) {
            const result = getRangesByTextArg(init);
            if (result) {
                return result;
            }
        }
    }
    else if (ts.isCallExpression(arg)) {
        const result = getRangesByTextArg(arg);
        if (result) {
            return result;
        }
    }
    console.error(`Unrecognized argument in verify.baselineRename: ${arg.getText()}`);
    return undefined;
}

function parseRangeVariable(arg: ts.Identifier | ts.ElementAccessExpression): string | undefined {
    const argName = ts.isIdentifier(arg) ? arg.text : (arg.expression as ts.Identifier).text;
    const file = arg.getSourceFile();
    const varStmts = file.statements.filter(ts.isVariableStatement);
    for (const varStmt of varStmts) {
        for (const decl of varStmt.declarationList.declarations) {
            if (ts.isArrayBindingPattern(decl.name) && decl.initializer?.getText().includes("ranges")) {
                for (let i = 0; i < decl.name.elements.length; i++) {
                    const elem = decl.name.elements[i];
                    if (ts.isBindingElement(elem) && ts.isIdentifier(elem.name) && elem.name.text === argName) {
                        // `const [range_0, ..., range_n, ...] = test.ranges();` and arg is `range_n`
                        if (elem.dotDotDotToken === undefined) {
                            return `f.Ranges()[${i}]`;
                        }
                        // `const [range_0, ..., ...rest] = test.ranges();` and arg is `rest[n]`
                        if (ts.isElementAccessExpression(arg)) {
                            return `f.Ranges()[${i + parseInt(arg.argumentExpression!.getText())}]`;
                        }
                        // `const [range_0, ..., ...rest] = test.ranges();` and arg is `rest`
                        return `ToAny(f.Ranges()[${i}:])...`;
                    }
                }
            }
        }
    }
    return undefined;
}

function getRangesByTextArg(arg: ts.CallExpression): string | undefined {
    if (arg.getText().startsWith("test.rangesByText()")) {
        if (ts.isStringLiteralLike(arg.arguments[0])) {
            return `ToAny(f.GetRangesByText().Get(${getGoStringLiteral(arg.arguments[0].text)}))...`;
        }
    }
    return undefined;
}

function parseBaselineQuickInfo(args: ts.NodeArray<ts.Expression>): VerifyBaselineQuickInfoCmd[] | undefined {
    if (args.length !== 0) {
        // !!!
        return undefined;
    }
    return [{
        kind: "verifyBaselineQuickInfo",
    }];
}

function parseQuickInfoArgs(funcName: string, args: readonly ts.Expression[]): VerifyQuickInfoCmd[] | undefined {
    // We currently don't support 'expectedTags'.
    switch (funcName) {
        case "quickInfoAt": {
            if (args.length < 1 || args.length > 3) {
                console.error(`Expected 1 or 2 arguments in quickInfoIs, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            let arg0;
            if (!(arg0 = getStringLiteralLike(args[0]))) {
                console.error(`Expected string literal for first argument in quickInfoAt, got ${args[0].getText()}`);
                return undefined;
            }
            const marker = getGoStringLiteral(arg0.text);
            let text: string | undefined;
            let arg1;
            if (args[1]) {
                if (!(arg1 = getStringLiteralLike(args[1]))) {
                    console.error(`Expected string literal for second argument in quickInfoAt, got ${args[1].getText()}`);
                    return undefined;
                }
                text = getGoStringLiteral(arg1.text);
            }
            let docs: string | undefined;
            let arg2;
            if (args[2]) {
                if (!(arg2 = getStringLiteralLike(args[2])) && args[2].getText() !== "undefined") {
                    console.error(`Expected string literal or undefined for third argument in quickInfoAt, got ${args[2].getText()}`);
                    return undefined;
                }
                if (arg2) {
                    docs = getGoStringLiteral(arg2.text);
                }
            }
            return [{
                kind: "quickInfoAt",
                marker,
                text,
                docs,
            }];
        }
        case "quickInfos": {
            const cmds: VerifyQuickInfoCmd[] = [];
            let arg0;
            if (args.length !== 1 || !(arg0 = getObjectLiteralExpression(args[0]))) {
                console.error(`Expected a single object literal argument in quickInfos, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            for (const prop of arg0.properties) {
                if (!ts.isPropertyAssignment(prop)) {
                    console.error(`Expected property assignment in quickInfos, got ${prop.getText()}`);
                    return undefined;
                }
                if (!(ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) || ts.isNumericLiteral(prop.name))) {
                    console.error(`Expected identifier or literal for property name in quickInfos, got ${prop.name.getText()}`);
                    return undefined;
                }
                const marker = getGoStringLiteral(prop.name.text);
                let text: string | undefined;
                let docs: string | undefined;
                let init;
                if (init = getArrayLiteralExpression(prop.initializer)) {
                    if (init.elements.length !== 2) {
                        console.error(`Expected two elements in array literal for quickInfos property, got ${init.getText()}`);
                        return undefined;
                    }
                    let textExp, docsExp;
                    if (!(textExp = getStringLiteralLike(init.elements[0])) || !(docsExp = getStringLiteralLike(init.elements[1]))) {
                        console.error(`Expected string literals in array literal for quickInfos property, got ${init.getText()}`);
                        return undefined;
                    }
                    text = getGoStringLiteral(textExp.text);
                    docs = getGoStringLiteral(docsExp.text);
                }
                else if (init = getStringLiteralLike(prop.initializer)) {
                    text = getGoStringLiteral(init.text);
                }
                else {
                    console.error(`Expected string literal or array literal for quickInfos property, got ${prop.initializer.getText()}`);
                    return undefined;
                }
                cmds.push({
                    kind: "quickInfoAt",
                    marker,
                    text,
                    docs,
                });
            }
            return cmds;
        }
        case "quickInfoExists":
            return [{
                kind: "quickInfoExists",
            }];
        case "notQuickInfoExists":
            return [{
                kind: "notQuickInfoExists",
            }];
        case "quickInfoIs": {
            if (args.length < 1 || args.length > 2) {
                console.error(`Expected 1 or 2 arguments in quickInfoIs, got ${args.map(arg => arg.getText()).join(", ")}`);
                return undefined;
            }
            let arg0;
            if (!(arg0 = getStringLiteralLike(args[0]))) {
                console.error(`Expected string literal for first argument in quickInfoIs, got ${args[0].getText()}`);
                return undefined;
            }
            const text = getGoStringLiteral(arg0.text);
            let docs: string | undefined;
            if (args[1]) {
                let arg1;
                if (!(arg1 = getStringLiteralLike(args[1]))) {
                    console.error(`Expected string literal for second argument in quickInfoIs, got ${args[1].getText()}`);
                    return undefined;
                }
                docs = getGoStringLiteral(arg1.text);
            }
            return [{
                kind: "quickInfoIs",
                text,
                docs,
            }];
        }
    }
    console.error(`Unrecognized quick info function: ${funcName}`);
    return undefined;
}

function parseBaselineSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd {
    if (args.length !== 0) {
        // All calls are currently empty!
        throw new Error("Expected no arguments in verify.baselineSignatureHelp");
    }
    return {
        kind: "verifyBaselineSignatureHelp",
    };
}

function parseSignatureHelpOptions(obj: ts.ObjectLiteralExpression): VerifySignatureHelpOptions | undefined {
    const options: VerifySignatureHelpOptions = {};

    for (const prop of obj.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            console.error(`Unexpected property in signatureHelp options: ${prop.getText()}`);
            continue;
        }
        const name = prop.name.text;
        const value = prop.initializer;

        switch (name) {
            case "marker": {
                if (ts.isStringLiteral(value)) {
                    options.marker = value.text;
                }
                else if (ts.isArrayLiteralExpression(value)) {
                    const markers: string[] = [];
                    for (const elem of value.elements) {
                        if (ts.isStringLiteral(elem)) {
                            markers.push(elem.text);
                        }
                        else {
                            console.error(`Expected string literal in marker array, got ${elem.getText()}`);
                            return undefined;
                        }
                    }
                    options.marker = markers;
                }
                else {
                    console.error(`Expected string or array for marker, got ${value.getText()}`);
                    return undefined;
                }
                break;
            }
            case "text": {
                const str = getStringLiteralLike(value);
                if (!str) {
                    console.error(`Expected string for text, got ${value.getText()}`);
                    return undefined;
                }
                options.text = str.text;
                break;
            }
            case "docComment": {
                const str = getStringLiteralLike(value);
                if (!str) {
                    console.error(`Expected string for docComment, got ${value.getText()}`);
                    return undefined;
                }
                options.docComment = str.text;
                break;
            }
            case "parameterCount": {
                const num = getNumericLiteral(value);
                if (!num) {
                    console.error(`Expected number for parameterCount, got ${value.getText()}`);
                    return undefined;
                }
                options.parameterCount = parseInt(num.text, 10);
                break;
            }
            case "parameterName": {
                const str = getStringLiteralLike(value);
                if (!str) {
                    console.error(`Expected string for parameterName, got ${value.getText()}`);
                    return undefined;
                }
                options.parameterName = str.text;
                break;
            }
            case "parameterSpan": {
                const str = getStringLiteralLike(value);
                if (!str) {
                    console.error(`Expected string for parameterSpan, got ${value.getText()}`);
                    return undefined;
                }
                options.parameterSpan = str.text;
                break;
            }
            case "parameterDocComment": {
                const str = getStringLiteralLike(value);
                if (!str) {
                    console.error(`Expected string for parameterDocComment, got ${value.getText()}`);
                    return undefined;
                }
                options.parameterDocComment = str.text;
                break;
            }
            case "overloadsCount": {
                const num = getNumericLiteral(value);
                if (!num) {
                    console.error(`Expected number for overloadsCount, got ${value.getText()}`);
                    return undefined;
                }
                options.overloadsCount = parseInt(num.text, 10);
                break;
            }
            case "overrideSelectedItemIndex": {
                const num = getNumericLiteral(value);
                if (!num) {
                    console.error(`Expected number for overrideSelectedItemIndex, got ${value.getText()}`);
                    return undefined;
                }
                options.overrideSelectedItemIndex = parseInt(num.text, 10);
                break;
            }
            case "triggerReason": {
                // triggerReason is an object like { kind: "invoked" } or { kind: "characterTyped", triggerCharacter: "(" }
                // For now, just pass it through as a string representation
                options.triggerReason = value.getText();
                break;
            }
            case "argumentCount":
                // ignore
                break;
            case "isVariadic": {
                if (value.kind === ts.SyntaxKind.TrueKeyword) {
                    options.isVariadic = true;
                }
                else if (value.kind === ts.SyntaxKind.FalseKeyword) {
                    options.isVariadic = false;
                }
                else {
                    console.error(`Expected boolean for isVariadic, got ${value.getText()}`);
                    return undefined;
                }
                break;
            }
            case "tags":
                // ignore
                break;
            default:
                console.error(`Unknown signatureHelp option: ${name}`);
                return undefined;
        }
    }
    return options;
}

function parseSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd[] | undefined {
    const allOptions: VerifySignatureHelpOptions[] = [];

    for (const arg of args) {
        if (ts.isObjectLiteralExpression(arg)) {
            const opts = parseSignatureHelpOptions(arg);
            if (!opts) {
                return undefined;
            }
            allOptions.push(opts);
        }
        else if (ts.isIdentifier(arg)) {
            // Could be a variable reference like `help2` - skip for now
            console.error(`signatureHelp with variable reference not supported: ${arg.getText()}`);
            return undefined;
        }
        else {
            console.error(`Unexpected argument type in signatureHelp: ${arg.getText()}`);
            return undefined;
        }
    }

    if (allOptions.length === 0) {
        console.error("signatureHelp requires at least one options object");
        return undefined;
    }

    return [{
        kind: "verifySignatureHelp",
        options: allOptions,
    }];
}

function parseNoSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd[] | undefined {
    const markers: string[] = [];

    for (const arg of args) {
        if (ts.isStringLiteral(arg)) {
            markers.push(arg.text);
        }
        else if (ts.isSpreadElement(arg)) {
            // Handle ...test.markerNames()
            const expr = arg.expression;
            if (
                ts.isCallExpression(expr) &&
                ts.isPropertyAccessExpression(expr.expression) &&
                ts.isIdentifier(expr.expression.expression) &&
                expr.expression.expression.text === "test" &&
                ts.isIdentifier(expr.expression.name) &&
                expr.expression.name.text === "markerNames"
            ) {
                // This means "all markers" - we'll handle this specially in the generator
                return [{
                    kind: "verifyNoSignatureHelp",
                    markers: ["...test.markerNames()"],
                }];
            }
            console.error(`Unsupported spread in noSignatureHelp: ${arg.getText()}`);
            return undefined;
        }
        else {
            console.error(`Unexpected argument in noSignatureHelp: ${arg.getText()}`);
            return undefined;
        }
    }

    return [{
        kind: "verifyNoSignatureHelp",
        markers,
    }];
}

interface SignatureHelpTriggerReason {
    kind: "invoked" | "characterTyped" | "retrigger";
    triggerCharacter?: string;
}

function parseTriggerReason(arg: ts.Expression): SignatureHelpTriggerReason | undefined | "undefined" {
    // Handle undefined literal
    if (ts.isIdentifier(arg) && arg.text === "undefined") {
        return "undefined";
    }

    if (!ts.isObjectLiteralExpression(arg)) {
        console.error(`Expected object literal for trigger reason, got ${arg.getText()}`);
        return undefined;
    }

    let kind: "invoked" | "characterTyped" | "retrigger" | undefined;
    let triggerCharacter: string | undefined;

    for (const prop of arg.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            console.error(`Unexpected property in trigger reason: ${prop.getText()}`);
            return undefined;
        }
        const name = prop.name.text;
        if (name === "kind") {
            if (!ts.isStringLiteral(prop.initializer)) {
                console.error(`Expected string literal for kind, got ${prop.initializer.getText()}`);
                return undefined;
            }
            const k = prop.initializer.text;
            if (k === "invoked" || k === "characterTyped" || k === "retrigger") {
                kind = k;
            }
            else {
                console.error(`Unknown trigger reason kind: ${k}`);
                return undefined;
            }
        }
        else if (name === "triggerCharacter") {
            if (!ts.isStringLiteral(prop.initializer)) {
                console.error(`Expected string literal for triggerCharacter, got ${prop.initializer.getText()}`);
                return undefined;
            }
            triggerCharacter = prop.initializer.text;
        }
    }

    if (!kind) {
        console.error(`Missing kind in trigger reason`);
        return undefined;
    }

    return { kind, triggerCharacter };
}

function parseSignatureHelpPresentForTriggerReason(args: ts.NodeArray<ts.Expression>): Cmd[] | undefined {
    if (args.length === 0) {
        console.error("signatureHelpPresentForTriggerReason requires at least one argument");
        return undefined;
    }

    const triggerReason = parseTriggerReason(args[0]);
    if (triggerReason === undefined) {
        return undefined;
    }

    const markers: string[] = [];
    for (let i = 1; i < args.length; i++) {
        const arg = args[i];
        if (ts.isStringLiteral(arg)) {
            markers.push(arg.text);
        }
        else {
            console.error(`Unexpected argument in signatureHelpPresentForTriggerReason: ${arg.getText()}`);
            return undefined;
        }
    }

    return [{
        kind: "verifySignatureHelpPresent",
        triggerReason: triggerReason === "undefined" ? undefined : triggerReason,
        markers,
    }];
}

function parseNoSignatureHelpForTriggerReason(args: ts.NodeArray<ts.Expression>): Cmd[] | undefined {
    if (args.length === 0) {
        console.error("noSignatureHelpForTriggerReason requires at least one argument");
        return undefined;
    }

    const triggerReason = parseTriggerReason(args[0]);
    if (triggerReason === undefined) {
        return undefined;
    }

    const markers: string[] = [];
    for (let i = 1; i < args.length; i++) {
        const arg = args[i];
        if (ts.isStringLiteral(arg)) {
            markers.push(arg.text);
        }
        else {
            console.error(`Unexpected argument in noSignatureHelpForTriggerReason: ${arg.getText()}`);
            return undefined;
        }
    }

    return [{
        kind: "verifyNoSignatureHelpForTriggerReason",
        triggerReason: triggerReason === "undefined" ? undefined : triggerReason,
        markers,
    }];
}

function parseBaselineSmartSelection(args: ts.NodeArray<ts.Expression>): Cmd {
    if (args.length !== 0) {
        // All calls are currently empty!
        throw new Error("Expected no arguments in verify.baselineSmartSelection");
    }
    return {
        kind: "verifyBaselineSmartSelection",
    };
}

function parseBaselineCallHierarchy(args: ts.NodeArray<ts.Expression>): Cmd {
    if (args.length !== 0) {
        throw new Error("Expected no arguments in verify.baselineCallHierarchy");
    }
    return {
        kind: "verifyBaselineCallHierarchy",
    };
}

function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutliningSpansCmd] | undefined {
    if (args.length === 0) {
        console.error("Expected at least one argument in verify.outliningSpansInCurrentFile");
        return undefined;
    }

    let spans: string = "";
    // Optional second argument for kind filter
    let foldingRangeKind: string | undefined;
    if (args.length > 1) {
        const kindArg = getStringLiteralLike(args[1]);
        if (!kindArg) {
            console.error(`Expected string literal for outlining kind, got ${args[1].getText()}`);
            return undefined;
        }
        switch (kindArg.text) {
            case "comment":
                foldingRangeKind = "lsproto.FoldingRangeKindComment";
                break;
            case "region":
                foldingRangeKind = "lsproto.FoldingRangeKindRegion";
                break;
            case "imports":
                foldingRangeKind = "lsproto.FoldingRangeKindImports";
                break;
            case "code":
                break;
            default:
                console.error(`Unknown folding range kind: ${kindArg.text}`);
                return undefined;
        }
    }

    return [{
        kind: "verifyOutliningSpans",
        spans,
        foldingRangeKind,
    }];
}

function parseKind(expr: ts.Expression): string | undefined {
    if (!ts.isStringLiteral(expr)) {
        console.error(`Expected string literal for kind, got ${expr.getText()}`);
        return undefined;
    }
    switch (expr.text) {
        case "primitive type":
        case "keyword":
            return "lsproto.CompletionItemKindKeyword";
        case "const":
        case "let":
        case "var":
        case "local var":
        case "alias":
        case "parameter":
            return "lsproto.CompletionItemKindVariable";
        case "property":
        case "getter":
        case "setter":
            return "lsproto.CompletionItemKindField";
        case "function":
        case "local function":
            return "lsproto.CompletionItemKindFunction";
        case "method":
        case "construct":
        case "call":
        case "index":
            return "lsproto.CompletionItemKindMethod";
        case "enum":
            return "lsproto.CompletionItemKindEnum";
        case "enum member":
            return "lsproto.CompletionItemKindEnumMember";
        case "module":
        case "external module name":
            return "lsproto.CompletionItemKindModule";
        case "class":
        case "type":
            return "lsproto.CompletionItemKindClass";
        case "interface":
            return "lsproto.CompletionItemKindInterface";
        case "warning":
            return "lsproto.CompletionItemKindText";
        case "script":
            return "lsproto.CompletionItemKindFile";
        case "directory":
            return "lsproto.CompletionItemKindFolder";
        case "string":
            return "lsproto.CompletionItemKindConstant";
        default:
            return "lsproto.CompletionItemKindProperty";
    }
}

const fileKindModifiers = new Set([".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"]);

function parseKindModifiers(expr: ts.Expression): { isOptional: boolean; isDeprecated: boolean; extensions: string[]; } | undefined {
    if (!ts.isStringLiteral(expr)) {
        console.error(`Expected string literal for kind modifiers, got ${expr.getText()}`);
        return undefined;
    }
    let isOptional = false;
    let isDeprecated = false;
    const extensions: string[] = [];
    const modifiers = expr.text.split(",");
    for (const modifier of modifiers) {
        switch (modifier) {
            case "optional":
                isOptional = true;
                break;
            case "deprecated":
                isDeprecated = true;
                break;
            default:
                if (fileKindModifiers.has(modifier)) {
                    extensions.push(modifier);
                }
        }
    }
    return {
        isOptional,
        isDeprecated,
        extensions,
    };
}

function parseSortText(expr: ts.Expression): string | undefined {
    if (ts.isCallExpression(expr) && expr.expression.getText() === "completion.SortText.Deprecated") {
        return `ls.DeprecateSortText(${parseSortText(expr.arguments[0])})`;
    }
    const text = expr.getText();
    switch (text) {
        case "completion.SortText.LocalDeclarationPriority":
            return "ls.SortTextLocalDeclarationPriority";
        case "completion.SortText.LocationPriority":
            return "ls.SortTextLocationPriority";
        case "completion.SortText.OptionalMember":
            return "ls.SortTextOptionalMember";
        case "completion.SortText.MemberDeclaredBySpreadAssignment":
            return "ls.SortTextMemberDeclaredBySpreadAssignment";
        case "completion.SortText.SuggestedClassMembers":
            return "ls.SortTextSuggestedClassMembers";
        case "completion.SortText.GlobalsOrKeywords":
            return "ls.SortTextGlobalsOrKeywords";
        case "completion.SortText.AutoImportSuggestions":
            return "ls.SortTextAutoImportSuggestions";
        case "completion.SortText.ClassMemberSnippets":
            return "ls.SortTextClassMemberSnippets";
        case "completion.SortText.JavascriptIdentifiers":
            return "ls.SortTextJavascriptIdentifiers";
        default:
            console.error(`Unrecognized sort text: ${text}`);
            return undefined; // !!! support deprecated/obj literal prop/etc
    }
}

function parseVerifyNavigateTo(args: ts.NodeArray<ts.Expression>): [VerifyNavToCmd] | undefined {
    const goArgs = [];
    for (const arg of args) {
        const result = parseVerifyNavigateToArg(arg);
        if (!result) {
            return undefined;
        }
        goArgs.push(result);
    }
    return [{
        kind: "verifyNavigateTo",
        args: goArgs,
    }];
}

function parseVerifyNavigateToArg(arg: ts.Expression): string | undefined {
    if (!ts.isObjectLiteralExpression(arg)) {
        console.error(`Expected object literal expression for verify.navigateTo argument, got ${arg.getText()}`);
        return undefined;
    }
    let prefs;
    const items = [];
    let pattern: string | undefined;
    for (const prop of arg.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            console.error(`Expected property assignment with identifier name for verify.navigateTo argument, got ${prop.getText()}`);
            return undefined;
        }
        const propName = prop.name.text;
        switch (propName) {
            case "pattern": {
                let patternInit = getStringLiteralLike(prop.initializer);
                if (!patternInit) {
                    console.error(`Expected string literal for pattern in verify.navigateTo argument, got ${prop.initializer.getText()}`);
                    return undefined;
                }
                pattern = getGoStringLiteral(patternInit.text);
                break;
            }
            case "fileName":
                // no longer supported
                continue;
            case "expected": {
                const init = prop.initializer;
                if (!ts.isArrayLiteralExpression(init)) {
                    console.error(`Expected array literal expression for expected property in verify.navigateTo argument, got ${init.getText()}`);
                    return undefined;
                }
                for (const elem of init.elements) {
                    const result = parseNavToItem(elem);
                    if (!result) {
                        return undefined;
                    }
                    items.push(result);
                }
                break;
            }
            case "excludeLibFiles": {
                if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
                    prefs = `&lsutil.UserPreferences{ExcludeLibrarySymbolsInNavTo: false}`;
                }
            }
        }
    }
    if (!prefs) {
        prefs = "nil";
    }
    return `{
        Pattern: ${pattern ? pattern : '""'},
        Preferences: ${prefs},
        Exact: PtrTo([]*lsproto.SymbolInformation{${items.length ? items.join(",\n") + ",\n" : ""}}),
    }`;
}

function parseVerifyNavTree(args: readonly ts.Expression[]): [VerifyNavTreeCmd] | undefined {
    // Ignore arguments and use baseline tests intead.
    return [{
        kind: "verifyNavigationTree",
    }];
}

function parseNavToItem(arg: ts.Expression): string | undefined {
    let item = getNodeOfKind(arg, ts.isObjectLiteralExpression);
    if (!item) {
        console.error(`Expected object literal expression for navigateTo item, got ${arg.getText()}`);
        return undefined;
    }
    const itemProps: string[] = [];
    for (const prop of item.properties) {
        if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
            console.error(`Expected property assignment with identifier name for navigateTo item, got ${prop.getText()}`);
            return undefined;
        }
        const propName = prop.name.text;
        const init = prop.initializer;
        switch (propName) {
            case "name": {
                let nameInit;
                if (!(nameInit = getStringLiteralLike(init))) {
                    console.error(`Expected string literal for name in navigateTo item, got ${init.getText()}`);
                    return undefined;
                }
                itemProps.push(`Name: ${getGoStringLiteral(nameInit.text)}`);
                break;
            }
            case "kind": {
                const goKind = getSymbolKind(init);
                if (!goKind) {
                    return undefined;
                }
                itemProps.push(`Kind: lsproto.${goKind}`);
                break;
            }
            case "kindModifiers": {
                if (init.getText().includes("deprecated")) {
                    itemProps.push(`Tags: &[]lsproto.SymbolTag{lsproto.SymbolTagDeprecated}`);
                }
                break;
            }
            case "range": {
                if (ts.isIdentifier(init) || (ts.isElementAccessExpression(init) && ts.isIdentifier(init.expression))) {
                    let parsedRange = parseRangeVariable(init);
                    if (parsedRange) {
                        itemProps.push(`Location: ${parsedRange}.LSLocation()`);
                        continue;
                    }
                }
                if (ts.isElementAccessExpression(init) && init.expression.getText() === "test.ranges()") {
                    itemProps.push(`Location: f.Ranges()[${parseInt(init.argumentExpression.getText())}].LSLocation()`);
                    continue;
                }
                console.error(`Expected range variable for range in navigateTo item, got ${init.getText()}`);
                return undefined;
            }
            case "containerName": {
                let nameInit;
                if (!(nameInit = getStringLiteralLike(init))) {
                    console.error(`Expected string literal for container name in navigateTo item, got ${init.getText()}`);
                    return undefined;
                }
                itemProps.push(`ContainerName: PtrTo(${getGoStringLiteral(nameInit.text)})`);
                break;
            }
            default:
                // ignore other properties
        }
    }
    return `{\n${itemProps.join(",\n")},\n}`;
}

function getSymbolKind(kind: ts.Expression): string | undefined {
    let result;
    if (!(result = getStringLiteralLike(kind))) {
        console.error(`Expected string literal for symbol kind, got ${kind.getText()}`);
        return undefined;
    }
    return getSymbolKindWorker(result.text);
}

function getSymbolKindWorker(kind: string): string {
    switch (kind) {
        case "script":
            return "SymbolKindFile";
        case "module":
            return "SymbolKindNamespace";
        case "class":
        case "local class":
            return "SymbolKindClass";
        case "interface":
            return "SymbolKindInterface";
        case "type":
            return "SymbolKindClass";
        case "enum":
            return "SymbolKindEnum";
        case "enum member":
            return "SymbolKindEnumMember";
        case "var":
        case "local var":
        case "using":
        case "await using":
            return "SymbolKindVariable";
        case "function":
        case "local function":
            return "SymbolKindFunction";
        case "method":
            return "SymbolKindMethod";
        case "getter":
        case "setter":
        case "property":
        case "accessor":
            return "SymbolKindProperty";
        case "constructor":
        case "construct":
            return "SymbolKindConstructor";
        case "call":
        case "index":
            return "SymbolKindFunction";
        case "parameter":
            return "SymbolKindVariable";
        case "type parameter":
            return "SymbolKindTypeParameter";
        case "primitive type":
            return "SymbolKindObject";
        case "const":
        case "let":
            return "SymbolKindVariable";
        case "directory":
            return "SymbolKindPackage";
        case "external module name":
            return "SymbolKindModule";
        case "string":
            return "SymbolKindString";
        case "type":
            return "SymbolKindClass";
        default:
            return "SymbolKindVariable";
    }
}

interface VerifyCompletionsCmd {
    kind: "verifyCompletions";
    marker: string;
    isNewIdentifierLocation?: true;
    args?: VerifyCompletionsArgs | "nil";
    andApplyCodeActionArgs?: VerifyApplyCodeActionArgs;
}

interface VerifyCompletionsArgs {
    includes?: string;
    excludes?: string;
    exact?: string;
    unsorted?: string;
}

interface VerifyApplyCodeActionArgs {
    name: string;
    source: string;
    description: string;
    newFileContent: string;
}

interface VerifyApplyCodeActionFromCompletionCmd {
    kind: "verifyApplyCodeActionFromCompletion";
    marker: string;
    options: string;
}

interface VerifyBaselineFindAllReferencesCmd {
    kind: "verifyBaselineFindAllReferences";
    markers: string[];
    ranges?: boolean;
}

interface VerifyBaselineGoToDefinitionCmd {
    kind: "verifyBaselineGoToDefinition" | "verifyBaselineGoToType" | "verifyBaselineGoToImplementation";
    markers: string[];
    boundSpan?: true;
    ranges?: boolean;
}

interface VerifyBaselineQuickInfoCmd {
    kind: "verifyBaselineQuickInfo";
}

interface VerifyBaselineSignatureHelpCmd {
    kind: "verifyBaselineSignatureHelp";
}

interface VerifyBaselineSmartSelection {
    kind: "verifyBaselineSmartSelection";
}

interface VerifyBaselineCallHierarchy {
    kind: "verifyBaselineCallHierarchy";
}

interface VerifyBaselineRenameCmd {
    kind: "verifyBaselineRename" | "verifyBaselineRenameAtRangesWithText";
    args: string[];
    preferences: string;
}

interface VerifyBaselineDocumentHighlightsCmd {
    kind: "verifyBaselineDocumentHighlights";
    args: string[];
    preferences: string;
}

interface VerifyBaselineInlayHintsCmd {
    kind: "verifyBaselineInlayHints";
    span: string;
    preferences: string;
}

interface VerifyImportFixAtPositionCmd {
    kind: "verifyImportFixAtPosition";
    expectedTexts: string[];
    preferences: string;
}

interface GoToCmd {
    kind: "goTo";
    // !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
    funcName: "marker" | "file" | "fileNumber" | "EOF" | "BOF" | "position" | "select";
    args: string[];
}

interface EditCmd {
    kind: "edit";
    goStatement: string;
}

interface VerifyQuickInfoCmd {
    kind: "quickInfoIs" | "quickInfoAt" | "quickInfoExists" | "notQuickInfoExists";
    marker?: string;
    text?: string;
    docs?: string;
}

interface VerifyRenameInfoCmd {
    kind: "renameInfoSucceeded" | "renameInfoFailed";
    preferences: string;
}

interface VerifyDiagnosticsCmd {
    kind: "verifyDiagnostics";
    arg: string;
    isSuggestion: boolean;
}

interface VerifyBaselineDiagnosticsCmd {
    kind: "verifyBaselineDiagnostics";
}

interface VerifyNavToCmd {
    kind: "verifyNavigateTo";
    args: string[];
}

interface VerifySignatureHelpOptions {
    marker?: string | string[];
    text?: string;
    docComment?: string;
    parameterCount?: number;
    parameterName?: string;
    parameterSpan?: string;
    parameterDocComment?: string;
    overloadsCount?: number;
    overrideSelectedItemIndex?: number;
    triggerReason?: string;
    isVariadic?: boolean;
}

interface VerifySignatureHelpCmd {
    kind: "verifySignatureHelp";
    options: VerifySignatureHelpOptions[];
}

interface VerifyNoSignatureHelpCmd {
    kind: "verifyNoSignatureHelp";
    markers: string[];
}

interface VerifySignatureHelpPresentCmd {
    kind: "verifySignatureHelpPresent";
    triggerReason?: SignatureHelpTriggerReason;
    markers: string[];
}

interface VerifyNoSignatureHelpForTriggerReasonCmd {
    kind: "verifyNoSignatureHelpForTriggerReason";
    triggerReason?: SignatureHelpTriggerReason;
    markers: string[];
}

interface VerifyOutliningSpansCmd {
    kind: "verifyOutliningSpans";
    spans: string;
    foldingRangeKind?: string;
}

interface VerifyNavTreeCmd {
    kind: "verifyNavigationTree";
}

type Cmd =
    | VerifyCompletionsCmd
    | VerifyApplyCodeActionFromCompletionCmd
    | VerifyBaselineFindAllReferencesCmd
    | VerifyBaselineDocumentHighlightsCmd
    | VerifyBaselineGoToDefinitionCmd
    | VerifyBaselineQuickInfoCmd
    | VerifyBaselineSignatureHelpCmd
    | VerifyBaselineSmartSelection
    | VerifySignatureHelpCmd
    | VerifyNoSignatureHelpCmd
    | VerifySignatureHelpPresentCmd
    | VerifyNoSignatureHelpForTriggerReasonCmd
    | VerifyBaselineCallHierarchy
    | GoToCmd
    | EditCmd
    | VerifyQuickInfoCmd
    | VerifyBaselineRenameCmd
    | VerifyRenameInfoCmd
    | VerifyNavToCmd
    | VerifyNavTreeCmd
    | VerifyBaselineInlayHintsCmd
    | VerifyImportFixAtPositionCmd
    | VerifyDiagnosticsCmd
    | VerifyBaselineDiagnosticsCmd
    | VerifyOutliningSpansCmd;

function generateVerifyOutliningSpans({ foldingRangeKind }: VerifyOutliningSpansCmd): string {
    if (foldingRangeKind) {
        return `f.VerifyOutliningSpans(t, ${foldingRangeKind})`;
    }
    return `f.VerifyOutliningSpans(t)`;
}

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string {
    let expectedList: string;
    if (args === "nil") {
        expectedList = "nil";
    }
    else {
        const expected = [];
        if (args?.includes) expected.push(`Includes: ${args.includes},`);
        if (args?.excludes) expected.push(`Excludes: ${args.excludes},`);
        if (args?.exact) expected.push(`Exact: ${args.exact},`);
        if (args?.unsorted) expected.push(`Unsorted: ${args.unsorted},`);
        // !!! isIncomplete
        const commitCharacters = isNewIdentifierLocation ? "[]string{}" : "DefaultCommitCharacters";
        expectedList = `&fourslash.CompletionsExpectedList{
    IsIncomplete: false,
    ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
        CommitCharacters: &${commitCharacters},
        EditRange: Ignored,
    },
    Items: &fourslash.CompletionsExpectedItems{
        ${expected.join("\n")}
    },
}`;
    }

    const call = `f.VerifyCompletions(t, ${marker}, ${expectedList})`;
    if (andApplyCodeActionArgs) {
        return `${call}.AndApplyCodeAction(t, &fourslash.CompletionsExpectedCodeAction{
            Name: ${getGoStringLiteral(andApplyCodeActionArgs.name)},
            Source: ${getGoStringLiteral(andApplyCodeActionArgs.source)},
            Description: ${getGoStringLiteral(andApplyCodeActionArgs.description)},
            NewFileContent: ${getGoMultiLineStringLiteral(andApplyCodeActionArgs.newFileContent)},
        })`;
    }
    return call;
}

function generateVerifyApplyCodeActionFromCompletion({ marker, options }: VerifyApplyCodeActionFromCompletionCmd): string {
    return `f.VerifyApplyCodeActionFromCompletion(t, ${marker}, ${options})`;
}

function generateBaselineFindAllReferences({ markers, ranges }: VerifyBaselineFindAllReferencesCmd): string {
    if (ranges || markers.length === 0) {
        return `f.VerifyBaselineFindAllReferences(t)`;
    }
    return `f.VerifyBaselineFindAllReferences(t, ${markers.join(", ")})`;
}

function generateBaselineDocumentHighlights({ args, preferences }: VerifyBaselineDocumentHighlightsCmd): string {
    return `f.VerifyBaselineDocumentHighlights(t, ${preferences}, ${args.join(", ")})`;
}

function generateBaselineGoToDefinition({ markers, ranges, kind, boundSpan }: VerifyBaselineGoToDefinitionCmd): string {
    const originalSelectionRange = boundSpan ? "true" : "false";
    switch (kind) {
        case "verifyBaselineGoToDefinition":
            if (ranges || markers.length === 0) {
                return `f.VerifyBaselineGoToDefinition(t, ${originalSelectionRange})`;
            }
            return `f.VerifyBaselineGoToDefinition(t, ${originalSelectionRange}, ${markers.join(", ")})`;
        case "verifyBaselineGoToType":
            if (ranges || markers.length === 0) {
                return `f.VerifyBaselineGoToTypeDefinition(t)`;
            }
            return `f.VerifyBaselineGoToTypeDefinition(t, ${markers.join(", ")})`;
        case "verifyBaselineGoToImplementation":
            if (ranges || markers.length === 0) {
                return `f.VerifyBaselineGoToImplementation(t)`;
            }
            return `f.VerifyBaselineGoToImplementation(t, ${markers.join(", ")})`;
    }
}

function generateGoToCommand({ funcName, args }: GoToCmd): string {
    const funcNameCapitalized = funcName.charAt(0).toUpperCase() + funcName.slice(1);
    return `f.GoTo${funcNameCapitalized}(t, ${args.join(", ")})`;
}

function generateQuickInfoCommand({ kind, marker, text, docs }: VerifyQuickInfoCmd): string {
    switch (kind) {
        case "quickInfoIs":
            return `f.VerifyQuickInfoIs(t, ${text!}, ${docs ? docs : `""`})`;
        case "quickInfoAt":
            return `f.VerifyQuickInfoAt(t, ${marker!}, ${text ? text : `""`}, ${docs ? docs : `""`})`;
        case "quickInfoExists":
            return `f.VerifyQuickInfoExists(t)`;
        case "notQuickInfoExists":
            return `f.VerifyNotQuickInfoExists(t)`;
    }
}

function generateBaselineRename({ kind, args, preferences }: VerifyBaselineRenameCmd): string {
    switch (kind) {
        case "verifyBaselineRename":
            return `f.VerifyBaselineRename(t, ${preferences}, ${args.join(", ")})`;
        case "verifyBaselineRenameAtRangesWithText":
            return `f.VerifyBaselineRenameAtRangesWithText(t, ${preferences}, ${args.join(", ")})`;
    }
}

function generateBaselineInlayHints({ span, preferences }: VerifyBaselineInlayHintsCmd): string {
    return `f.VerifyBaselineInlayHints(t, ${span}, ${preferences})`;
}

function generateImportFixAtPosition({ expectedTexts, preferences }: VerifyImportFixAtPositionCmd): string {
    // Handle empty array case
    if (expectedTexts.length === 1 && expectedTexts[0] === "") {
        return `f.VerifyImportFixAtPosition(t, []string{}, ${preferences})`;
    }
    return `f.VerifyImportFixAtPosition(t, []string{\n${expectedTexts.join(",\n")},\n}, ${preferences})`;
}

function generateSignatureHelpExpected(opts: VerifySignatureHelpOptions): string {
    const fields: string[] = [];

    if (opts.text !== undefined) {
        fields.push(`Text: ${getGoStringLiteral(opts.text)}`);
    }
    if (opts.docComment !== undefined) {
        fields.push(`DocComment: ${getGoStringLiteral(opts.docComment)}`);
    }
    if (opts.parameterCount !== undefined) {
        fields.push(`ParameterCount: ${opts.parameterCount}`);
    }
    if (opts.parameterName !== undefined) {
        fields.push(`ParameterName: ${getGoStringLiteral(opts.parameterName)}`);
    }
    if (opts.parameterSpan !== undefined) {
        fields.push(`ParameterSpan: ${getGoStringLiteral(opts.parameterSpan)}`);
    }
    if (opts.parameterDocComment !== undefined) {
        fields.push(`ParameterDocComment: ${getGoStringLiteral(opts.parameterDocComment)}`);
    }
    if (opts.overloadsCount !== undefined) {
        fields.push(`OverloadsCount: ${opts.overloadsCount}`);
    }
    if (opts.overrideSelectedItemIndex !== undefined) {
        fields.push(`OverrideSelectedItemIndex: ${opts.overrideSelectedItemIndex}`);
    }
    if (opts.isVariadic !== undefined) {
        fields.push(`IsVariadic: ${opts.isVariadic}`);
        fields.push(`IsVariadicSet: true`);
    }

    return `fourslash.VerifySignatureHelpOptions{${fields.join(", ")}}`;
}

function generateSignatureHelp({ options }: VerifySignatureHelpCmd): string {
    const lines: string[] = [];

    for (const opts of options) {
        const expected = generateSignatureHelpExpected(opts);

        // Add comments for unsupported options
        const unsupportedComments: string[] = [];

        if (opts.marker !== undefined) {
            const markers = Array.isArray(opts.marker) ? opts.marker : [opts.marker];
            for (const marker of markers) {
                lines.push(`f.GoToMarker(t, ${getGoStringLiteral(marker)})`);
                for (const comment of unsupportedComments) {
                    lines.push(comment);
                }
                lines.push(`f.VerifySignatureHelp(t, ${expected})`);
            }
        }
        else {
            // No marker specified, use current position
            for (const comment of unsupportedComments) {
                lines.push(comment);
            }
            lines.push(`f.VerifySignatureHelp(t, ${expected})`);
        }
    }

    return lines.join("\n");
}

function generateNoSignatureHelp({ markers }: VerifyNoSignatureHelpCmd): string {
    if (markers.length === 1 && markers[0] === "...test.markerNames()") {
        // All markers
        return `f.VerifyNoSignatureHelpForMarkers(t, f.MarkerNames()...)`;
    }
    if (markers.length === 0) {
        // Current position
        return `f.VerifyNoSignatureHelp(t)`;
    }
    // Specific markers
    const markerArgs = markers.map(m => getGoStringLiteral(m)).join(", ");
    return `f.VerifyNoSignatureHelpForMarkers(t, ${markerArgs})`;
}

function generateTriggerContext(triggerReason: SignatureHelpTriggerReason | undefined): string {
    if (!triggerReason) {
        return "nil";
    }
    switch (triggerReason.kind) {
        case "invoked":
            return `&lsproto.SignatureHelpContext{TriggerKind: lsproto.SignatureHelpTriggerKindInvoked}`;
        case "characterTyped":
            return `&lsproto.SignatureHelpContext{TriggerKind: lsproto.SignatureHelpTriggerKindTriggerCharacter, TriggerCharacter: PtrTo(${getGoStringLiteral(triggerReason.triggerCharacter ?? "")}), IsRetrigger: false}`;
        case "retrigger":
            return `&lsproto.SignatureHelpContext{TriggerKind: lsproto.SignatureHelpTriggerKindTriggerCharacter, TriggerCharacter: PtrTo(${getGoStringLiteral(triggerReason.triggerCharacter ?? "")}), IsRetrigger: true}`;
        default:
            throw new Error(`Unknown trigger reason kind: ${triggerReason}`);
    }
}

function generateSignatureHelpPresent({ triggerReason, markers }: VerifySignatureHelpPresentCmd): string {
    const context = generateTriggerContext(triggerReason);
    if (markers.length === 0) {
        // Current position
        return `f.VerifySignatureHelpPresent(t, ${context})`;
    }
    // Specific markers
    const markerArgs = markers.map(m => getGoStringLiteral(m)).join(", ");
    return `f.VerifySignatureHelpPresentForMarkers(t, ${context}, ${markerArgs})`;
}

function generateNoSignatureHelpForTriggerReason({ triggerReason, markers }: VerifyNoSignatureHelpForTriggerReasonCmd): string {
    const context = generateTriggerContext(triggerReason);
    if (markers.length === 0) {
        // Current position
        return `f.VerifyNoSignatureHelpWithContext(t, ${context})`;
    }
    // Specific markers
    const markerArgs = markers.map(m => getGoStringLiteral(m)).join(", ");
    return `f.VerifyNoSignatureHelpForMarkersWithContext(t, ${context}, ${markerArgs})`;
}

function generateNavigateTo({ args }: VerifyNavToCmd): string {
    return `f.VerifyWorkspaceSymbol(t, []*fourslash.VerifyWorkspaceSymbolCase{\n${args.join(", ")}})`;
}

function generateCmd(cmd: Cmd): string {
    switch (cmd.kind) {
        case "verifyCompletions":
            return generateVerifyCompletions(cmd);
        case "verifyApplyCodeActionFromCompletion":
            return generateVerifyApplyCodeActionFromCompletion(cmd);
        case "verifyBaselineFindAllReferences":
            return generateBaselineFindAllReferences(cmd);
        case "verifyBaselineDocumentHighlights":
            return generateBaselineDocumentHighlights(cmd);
        case "verifyBaselineGoToDefinition":
        case "verifyBaselineGoToType":
        case "verifyBaselineGoToImplementation":
            return generateBaselineGoToDefinition(cmd);
        case "verifyBaselineQuickInfo":
            // Quick Info -> Hover
            return `f.VerifyBaselineHover(t)`;
        case "verifyBaselineSignatureHelp":
            return `f.VerifyBaselineSignatureHelp(t)`;
        case "verifyBaselineSmartSelection":
            return `f.VerifyBaselineSelectionRanges(t)`;
        case "verifyBaselineCallHierarchy":
            return `f.VerifyBaselineCallHierarchy(t)`;
        case "goTo":
            return generateGoToCommand(cmd);
        case "edit":
            return cmd.goStatement;
        case "quickInfoAt":
        case "quickInfoIs":
        case "quickInfoExists":
        case "notQuickInfoExists":
            return generateQuickInfoCommand(cmd);
        case "verifyBaselineRename":
        case "verifyBaselineRenameAtRangesWithText":
            return generateBaselineRename(cmd);
        case "renameInfoSucceeded":
            return `f.VerifyRenameSucceeded(t, ${cmd.preferences})`;
        case "renameInfoFailed":
            return `f.VerifyRenameFailed(t, ${cmd.preferences})`;
        case "verifyBaselineInlayHints":
            return generateBaselineInlayHints(cmd);
        case "verifyImportFixAtPosition":
            return generateImportFixAtPosition(cmd);
        case "verifyDiagnostics":
            const funcName = cmd.isSuggestion ? "VerifySuggestionDiagnostics" : "VerifyNonSuggestionDiagnostics";
            return `f.${funcName}(t, ${cmd.arg})`;
        case "verifyBaselineDiagnostics":
            return `f.VerifyBaselineNonSuggestionDiagnostics(t)`;
        case "verifyNavigateTo":
            return generateNavigateTo(cmd);
        case "verifySignatureHelp":
            return generateSignatureHelp(cmd);
        case "verifyNoSignatureHelp":
            return generateNoSignatureHelp(cmd);
        case "verifySignatureHelpPresent":
            return generateSignatureHelpPresent(cmd);
        case "verifyNoSignatureHelpForTriggerReason":
            return generateNoSignatureHelpForTriggerReason(cmd);
        case "verifyOutliningSpans":
            return generateVerifyOutliningSpans(cmd);
        case "verifyNavigationTree":
            return `f.VerifyBaselineDocumentSymbol(t)`;
        default:
            let neverCommand: never = cmd;
            throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);
    }
}

interface GoTest {
    name: string;
    content: string;
    commands: Cmd[];
}

function generateGoTest(failingTests: Set<string>, test: GoTest, isServer: boolean): string {
    const testName = (test.name[0].toUpperCase() + test.name.substring(1)).replaceAll("-", "_").replaceAll(/[^a-zA-Z0-9_]/g, "");
    const content = test.content;
    const commands = test.commands.map(cmd => generateCmd(cmd)).join("\n");
    const imports = [`"github.com/microsoft/typescript-go/internal/fourslash"`];
    // Only include these imports if the commands use them to avoid unused import errors.
    if (commands.includes("core.")) {
        imports.unshift(`"github.com/microsoft/typescript-go/internal/core"`);
    }
    if (commands.includes("ls.")) {
        imports.push(`"github.com/microsoft/typescript-go/internal/ls"`);
    }
    if (commands.includes("lsutil.")) {
        imports.push(`"github.com/microsoft/typescript-go/internal/ls/lsutil"`);
    }
    if (commands.includes("lsproto.")) {
        imports.push(`"github.com/microsoft/typescript-go/internal/lsp/lsproto"`);
    }
    if (usesFourslashUtil(commands)) {
        imports.push(`. "github.com/microsoft/typescript-go/internal/fourslash/tests/util"`);
    }
    imports.push(`"github.com/microsoft/typescript-go/internal/testutil"`);
    const template = `package fourslash_test

import (
	"testing"

    ${imports.join("\n\t")}
)

func Test${testName}(t *testing.T) {
    t.Parallel()
    ${failingTests.has(testName) ? "t.Skip()" : ""}
    defer testutil.RecoverAndFail(t, "Panic on fourslash test")
	const content = ${content}
    f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
    defer done()
    ${isServer ? `f.MarkTestAsStradaServer()\n` : ""}${commands}
}`;
    return template;
}

function usesFourslashUtil(goTxt: string): boolean {
    for (const [_, constant] of completionConstants) {
        if (goTxt.includes(constant)) {
            return true;
        }
    }
    for (const [_, constant] of completionPlus) {
        if (goTxt.includes(constant)) {
            return true;
        }
    }
    return goTxt.includes("Ignored")
        || goTxt.includes("DefaultCommitCharacters")
        || goTxt.includes("PtrTo")
        || goTxt.includes("ToAny");
}

function getNodeOfKind<T extends ts.Node>(node: ts.Node, hasKind: (n: ts.Node) => n is T): T | undefined {
    if (hasKind(node)) {
        return node;
    }
    if (ts.isIdentifier(node)) {
        const init = getInitializer(node);
        if (init && hasKind(init)) {
            return init;
        }
    }
    return undefined;
}

function getObjectLiteralExpression(node: ts.Node): ts.ObjectLiteralExpression | undefined {
    return getNodeOfKind(node, ts.isObjectLiteralExpression);
}

function getStringLiteralLike(node: ts.Node): ts.StringLiteralLike | undefined {
    return getNodeOfKind(node, ts.isStringLiteralLike);
}

function getNumericLiteral(node: ts.Node): ts.NumericLiteral | undefined {
    return getNodeOfKind(node, ts.isNumericLiteral);
}

function getArrayLiteralExpression(node: ts.Node): ts.ArrayLiteralExpression | undefined {
    return getNodeOfKind(node, ts.isArrayLiteralExpression);
}

function getInitializer(name: ts.Identifier): ts.Expression | undefined {
    const file = name.getSourceFile();
    const varStmts = file.statements.filter(ts.isVariableStatement);
    for (const varStmt of varStmts) {
        const decls = varStmt.declarationList.declarations.filter(varDecl => {
            if (ts.isIdentifier(varDecl.name)) {
                return varDecl.name.text === name.text;
            }
            return false;
        });
        if (decls[0]) {
            return decls[0].initializer;
        }
    }
    return undefined;
}

if (url.fileURLToPath(import.meta.url) == process.argv[1]) {
    main();
}
